diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index eb87b8f77a..44e7ec383f 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -20,9 +20,6 @@ from .version import inventreeVersion, inventreeInstanceName from plugins import plugins as inventree_plugins -# Load barcode plugins -print("Loading barcode plugins") -barcode_plugins = inventree_plugins.load_barcode_plugins() print("Loading action plugins") action_plugins = inventree_plugins.load_action_plugins() @@ -100,66 +97,3 @@ class ActionPluginView(APIView): 'error': _("No matching action found"), "action": action, }) - - -class BarcodePluginView(APIView): - """ - Endpoint for handling barcode scan requests. - - Barcode data are decoded by the client application, - and sent to this endpoint (as a JSON object) for validation. - - A barcode could follow the internal InvenTree barcode format, - or it could match to a third-party barcode format (e.g. Digikey). - - """ - - permission_classes = [ - permissions.IsAuthenticated, - ] - - def post(self, request, *args, **kwargs): - - response = {} - - barcode_data = request.data.get('barcode', None) - - print("Barcode data:") - print(barcode_data) - - if barcode_data is None: - response['error'] = _('No barcode data provided') - else: - # Look for a barcode plugin that knows how to handle the data - for plugin_class in barcode_plugins: - - # Instantiate the plugin with the provided plugin data - plugin = plugin_class(barcode_data) - - if plugin.validate(): - - # Plugin should return a dict response - response = plugin.decode() - - if type(response) is dict: - if 'success' not in response.keys() and 'error' not in response.keys(): - response['success'] = _('Barcode successfully decoded') - else: - response = { - 'error': _('Barcode plugin returned incorrect response') - } - - response['plugin'] = plugin.plugin_name() - response['hash'] = plugin.hash() - - break - - if 'error' not in response and 'success' not in response: - response = { - 'error': _('Unknown barcode format'), - } - - # Include the original barcode data - response['barcode_data'] = barcode_data - - return Response(response) diff --git a/InvenTree/InvenTree/plugins.py b/InvenTree/InvenTree/plugins.py new file mode 100644 index 0000000000..8da725bf9c --- /dev/null +++ b/InvenTree/InvenTree/plugins.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +import inspect +import importlib +import pkgutil + + +def iter_namespace(pkg): + + return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".") + + +def get_modules(pkg): + # Return all modules in a given package + return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)] + + +def get_classes(module): + # Return all classes in a given module + return inspect.getmembers(module, inspect.isclass) + + +def get_plugins(pkg, baseclass): + """ + Return a list of all modules under a given package. + + - Modules must be a subclass of the provided 'baseclass' + - Modules must have a non-empty PLUGIN_NAME parameter + """ + + plugins = [] + + modules = get_modules(pkg) + + # Iterate through each module in the package + for mod in modules: + # Iterate through each class in the module + for item in get_classes(mod): + plugin = item[1] + if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: + plugins.append(plugin) + + return plugins diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 7861be295a..ccf0671d56 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -82,12 +82,25 @@ float: left; } +.navbar-barcode-li { + border-left: none; + border-right: none; +} + .navbar-nav > li { border-left: 1px solid; border-right: 1px solid; border-color: #eee; } +.navbar-form { + padding-right: 3px; +} + +#barcode-scan { + margin-top: 8px; +} + .icon-header { margin-right: 10px; } @@ -259,6 +272,16 @@ margin-left: 1px; } +.dropdown-buttons { + display: inline-block +} + +.dropdown-menu .open{ + z-index: 1000; + position: relative; + overflow: visible; +} + /* Styles for table buttons and filtering */ .button-toolbar .btn { margin-left: 1px; @@ -455,6 +478,7 @@ .media-body { padding-top: 10px; + overflow: visible; } .navigation { diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/InvenTree/static/script/inventree/modals.js index 24a1a38ed3..5b76203927 100644 --- a/InvenTree/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/InvenTree/static/script/inventree/modals.js @@ -166,14 +166,28 @@ function modalSetContent(modal, content='') { } +function modalSetSubmitText(modal, text) { + if (text) { + $(modal).find('#modal-form-submit').html(text); + } +} + + +function modalSetCloseText(modal, text) { + if (text) { + $(modal).find('#modal-form-close').html(text); + } +} + + function modalSetButtonText(modal, submit_text, close_text) { /* Set the button text for a modal form * * submit_text - text for the form submit button * close_text - text for the form dismiss button */ - $(modal).find("#modal-form-submit").html(submit_text); - $(modal).find("#modal-form-close").html(close_text); + modalSetSubmitText(modal, submit_text); + modalSetCloseText(modal, close_text); } @@ -442,7 +456,8 @@ function attachSecondaryModal(modal, options) { */ var select = '#id_' + options.field; - var option = new Option(response.text, response.pk, true, true) + + var option = new Option(response.text, response.pk, true, true); $(modal).find(select).append(option).trigger('change'); } diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 7068593e6b..69d701def0 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -79,30 +79,3 @@ class APITests(APITestCase): self.assertIn('instance', data) self.assertEquals('InvenTree', data['server']) - - def test_barcode_fail(self): - # Test barcode endpoint without auth - response = self.client.post(reverse('api-barcode-plugin'), format='json') - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_barcode(self): - """ Test the barcode endpoint """ - - self.tokenAuth() - - url = reverse('api-barcode-plugin') - - data = { - 'barcode': { - 'asdlaksdfalsdkfsa;fdlkasd;' - }, - } - - response = self.client.post(url, format='json', data=data) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.assertIn('error', response.data) - self.assertIn('barcode_data', response.data) - self.assertEqual(response.data['error'], 'Unknown barcode format') diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 711df33798..c2d0d0f48f 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -20,6 +20,7 @@ from stock.urls import stock_urls from build.urls import build_urls from order.urls import order_urls +from barcode.api import barcode_api_urls from common.api import common_api_urls from part.api import part_api_urls, bom_api_urls from company.api import company_api_urls @@ -37,13 +38,15 @@ from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView from .views import DynamicJsView -from .api import InfoView, BarcodePluginView, ActionPluginView +from .api import InfoView +from .api import ActionPluginView from users.urls import user_urls admin.site.site_header = "InvenTree Admin" apipatterns = [ + url(r'^barcode/', include(barcode_api_urls)), url(r'^common/', include(common_api_urls)), url(r'^part/', include(part_api_urls)), url(r'^bom/', include(bom_api_urls)), @@ -56,7 +59,6 @@ apipatterns = [ url(r'^user/', include(user_urls)), # Plugin endpoints - url(r'^barcode/', BarcodePluginView.as_view(), name='api-barcode-plugin'), url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'), # InvenTree information endpoint @@ -74,7 +76,9 @@ settings_urls = [ url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'), ] +# Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ + url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.html'), name='barcode.js'), url(r'^part.js', DynamicJsView.as_view(template_name='js/part.html'), name='part.js'), url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.html'), name='stock.js'), url(r'^build.js', DynamicJsView.as_view(template_name='js/build.html'), name='build.js'), diff --git a/InvenTree/plugins/barcode/__init__.py b/InvenTree/barcode/__init__.py similarity index 100% rename from InvenTree/plugins/barcode/__init__.py rename to InvenTree/barcode/__init__.py diff --git a/InvenTree/barcode/api.py b/InvenTree/barcode/api.py new file mode 100644 index 0000000000..cecdf0b349 --- /dev/null +++ b/InvenTree/barcode/api.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- + +from django.urls import reverse +from django.conf.urls import url +from django.utils.translation import ugettext as _ + +from rest_framework.exceptions import ValidationError +from rest_framework import permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from stock.models import StockItem +from stock.serializers import StockItemSerializer + +from barcode.barcode import load_barcode_plugins, hash_barcode + + +class BarcodeScan(APIView): + """ + Endpoint for handling generic barcode scan requests. + + Barcode data are decoded by the client application, + and sent to this endpoint (as a JSON object) for validation. + + A barcode could follow the internal InvenTree barcode format, + or it could match to a third-party barcode format (e.g. Digikey). + + When a barcode is sent to the server, the following parameters must be provided: + + - barcode: The raw barcode data + + plugins: + Third-party barcode formats may be supported using 'plugins' + (more information to follow) + + hashing: + Barcode hashes are calculated using MD5 + + """ + + permission_classes = [ + permissions.IsAuthenticated, + ] + + def post(self, request, *args, **kwargs): + """ + Respond to a barcode POST request + """ + + data = request.data + + if 'barcode' not in data: + raise ValidationError({'barcode': _('Must provide barcode_data parameter')}) + + plugins = load_barcode_plugins() + + barcode_data = data.get('barcode') + + # Look for a barcode plugin which knows how to deal with this barcode + plugin = None + + for plugin_class in plugins: + plugin_instance = plugin_class(barcode_data) + + if plugin_instance.validate(): + plugin = plugin_instance + break + + match_found = False + response = {} + + response['barcode_data'] = barcode_data + + # A plugin has been found! + if plugin is not None: + + # Try to associate with a stock item + item = plugin.getStockItem() + + if item is None: + item = plugin.getStockItemByHash() + + if item is not None: + response['stockitem'] = plugin.renderStockItem(item) + response['url'] = reverse('stock-item-detail', kwargs={'pk': item.id}) + match_found = True + + # Try to associate with a stock location + loc = plugin.getStockLocation() + + if loc is not None: + response['stocklocation'] = plugin.renderStockLocation(loc) + response['url'] = reverse('location-detail', kwargs={'pk': loc.id}) + match_found = True + + # Try to associate with a part + part = plugin.getPart() + + if part is not None: + response['part'] = plugin.renderPart(part) + response['url'] = reverse('part-detail', kwargs={'pk': part.id}) + match_found = True + + response['hash'] = plugin.hash() + response['plugin'] = plugin.name + + # No plugin is found! + # However, the hash of the barcode may still be associated with a StockItem! + else: + hash = hash_barcode(barcode_data) + + response['hash'] = hash + response['plugin'] = None + + # Try to look for a matching StockItem + try: + item = StockItem.objects.get(uid=hash) + serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True) + response['stockitem'] = serializer.data + response['url'] = reverse('stock-item-detail', kwargs={'pk': item.id}) + match_found = True + except StockItem.DoesNotExist: + pass + + if not match_found: + response['error'] = _('No match found for barcode data') + else: + response['success'] = _('Match found for barcode data') + + return Response(response) + + +class BarcodeAssign(APIView): + """ + Endpoint for assigning a barcode to a stock item. + + - This only works if the barcode is not already associated with an object in the database + - If the barcode does not match an object, then the barcode hash is assigned to the StockItem + """ + + permission_classes = [ + permissions.IsAuthenticated + ] + + def post(self, request, *args, **kwargs): + + data = request.data + + if 'barcode' not in data: + raise ValidationError({'barcode': _('Must provide barcode_data parameter')}) + + if 'stockitem' not in data: + raise ValidationError({'stockitem': _('Must provide stockitem parameter')}) + + barcode_data = data['barcode'] + + try: + item = StockItem.objects.get(pk=data['stockitem']) + except (ValueError, StockItem.DoesNotExist): + raise ValidationError({'stockitem': _('No matching stock item found')}) + + plugins = load_barcode_plugins() + + plugin = None + + for plugin_class in plugins: + plugin_instance = plugin_class(barcode_data) + + if plugin_instance.validate(): + plugin = plugin_instance + break + + match_found = False + + response = {} + + response['barcode_data'] = barcode_data + + # Matching plugin was found + if plugin is not None: + + hash = plugin.hash() + response['hash'] = hash + response['plugin'] = plugin.name + + # Ensure that the barcode does not already match a database entry + + if plugin.getStockItem() is not None: + match_found = True + response['error'] = _('Barcode already matches StockItem object') + + if plugin.getStockLocation() is not None: + match_found = True + response['error'] = _('Barcode already matches StockLocation object') + + if plugin.getPart() is not None: + match_found = True + response['error'] = _('Barcode already matches Part object') + + if not match_found: + item = plugin.getStockItemByHash() + + if item is not None: + response['error'] = _('Barcode hash already matches StockItem object') + match_found = True + + else: + hash = hash_barcode(barcode_data) + + response['hash'] = hash + response['plugin'] = None + + # Lookup stock item by hash + try: + item = StockItem.objects.get(uid=hash) + response['error'] = _('Barcode hash already matches StockItem object') + match_found = True + except StockItem.DoesNotExist: + pass + + if not match_found: + response['success'] = _('Barcode associated with StockItem') + + # Save the barcode hash + item.uid = response['hash'] + item.save() + + serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True) + response['stockitem'] = serializer.data + + return Response(response) + + +barcode_api_urls = [ + + url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'), + + # Catch-all performs barcode 'scan' + url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'), +] diff --git a/InvenTree/barcode/barcode.py b/InvenTree/barcode/barcode.py new file mode 100644 index 0000000000..c9fac02035 --- /dev/null +++ b/InvenTree/barcode/barcode.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +import hashlib + +from InvenTree import plugins as InvenTreePlugins +from barcode import plugins as BarcodePlugins + +from stock.models import StockItem +from stock.serializers import StockItemSerializer, LocationSerializer +from part.serializers import PartSerializer + + +def hash_barcode(barcode_data): + """ + Calculate an MD5 hash of barcode data + """ + + hash = hashlib.md5(str(barcode_data).encode()) + return str(hash.hexdigest()) + + +class BarcodePlugin: + """ + Base class for barcode handling. + Custom barcode plugins should extend this class as necessary. + """ + + # Override the barcode plugin name for each sub-class + PLUGIN_NAME = "" + + @property + def name(self): + return self.PLUGIN_NAME + + def __init__(self, barcode_data): + """ + Initialize the BarcodePlugin instance + + Args: + barcode_data - The raw barcode data + """ + + self.data = barcode_data + + def getStockItem(self): + """ + Attempt to retrieve a StockItem associated with this barcode. + Default implementation returns None + """ + + return None + + def getStockItemByHash(self): + """ + Attempt to retrieve a StockItem associated with this barcode, + based on the barcode hash. + """ + + try: + item = StockItem.objects.get(uid=self.hash()) + return item + except StockItem.DoesNotExist: + return None + + def renderStockItem(self, item): + """ + Render a stock item to JSON response + """ + + serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True) + return serializer.data + + def getStockLocation(self): + """ + Attempt to retrieve a StockLocation associated with this barcode. + Default implementation returns None + """ + + return None + + def renderStockLocation(self, loc): + """ + Render a stock location to a JSON response + """ + + serializer = LocationSerializer(loc) + return serializer.data + + def getPart(self): + """ + Attempt to retrieve a Part associated with this barcode. + Default implementation returns None + """ + + return None + + def renderPart(self, part): + """ + Render a part to JSON response + """ + + serializer = PartSerializer(part) + return serializer.data + + def hash(self): + """ + Calculate a hash for the barcode data. + This is supposed to uniquely identify the barcode contents, + at least within the bardcode sub-type. + + The default implementation simply returns an MD5 hash of the barcode data, + encoded to a string. + + This may be sufficient for most applications, but can obviously be overridden + by a subclass. + + """ + + return hash_barcode(self.data) + + def validate(self): + """ + Default implementation returns False + """ + return False + + +def load_barcode_plugins(debug=False): + """ + Function to load all barcode plugins + """ + + if debug: + print("Loading barcode plugins") + + plugins = InvenTreePlugins.get_plugins(BarcodePlugins, BarcodePlugin) + + if debug: + if len(plugins) > 0: + print("Discovered {n} plugins:".format(n=len(plugins))) + + for p in plugins: + print(" - {p}".format(p=p.PLUGIN_NAME)) + else: + print("No barcode plugins found") + + return plugins diff --git a/InvenTree/barcode/plugins/digikey_barcode.py b/InvenTree/barcode/plugins/digikey_barcode.py new file mode 100644 index 0000000000..1a0825e03c --- /dev/null +++ b/InvenTree/barcode/plugins/digikey_barcode.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +""" +DigiKey barcode decoding +""" + +from barcode.barcode import BarcodePlugin + + +class DigikeyBarcodePlugin(BarcodePlugin): + + PLUGIN_NAME = "DigikeyBarcode" + + def validate(self): + """ + TODO: Validation of Digikey barcodes. + """ + + return False diff --git a/InvenTree/barcode/plugins/inventree_barcode.py b/InvenTree/barcode/plugins/inventree_barcode.py new file mode 100644 index 0000000000..821cdc9c88 --- /dev/null +++ b/InvenTree/barcode/plugins/inventree_barcode.py @@ -0,0 +1,108 @@ +""" +The InvenTreeBarcodePlugin validates barcodes generated by InvenTree itself. +It can be used as a template for developing third-party barcode plugins. + +The data format is very simple, and maps directly to database objects, +via the "id" parameter. + +Parsing an InvenTree barcode simply involves validating that the +references model objects actually exist in the database. +""" + +# -*- coding: utf-8 -*- + +import json + +from barcode.barcode import BarcodePlugin + +from stock.models import StockItem, StockLocation +from part.models import Part + +from rest_framework.exceptions import ValidationError + + +class InvenTreeBarcodePlugin(BarcodePlugin): + + PLUGIN_NAME = "InvenTreeBarcode" + + def validate(self): + """ + An "InvenTree" barcode must be a jsonnable-dict with the following tags: + + { + 'tool': 'InvenTree', + 'version': + } + + """ + + # The data must either be dict or be able to dictified + if type(self.data) is dict: + pass + elif type(self.data) is str: + try: + self.data = json.loads(self.data) + except json.JSONDecodeError: + return False + else: + return False + + for key in ['tool', 'version']: + if key not in self.data.keys(): + return False + + if not self.data['tool'] == 'InvenTree': + return False + + return True + + def getStockItem(self): + + for k in self.data.keys(): + if k.lower() == 'stockitem': + try: + pk = self.data[k]['id'] + except (AttributeError, KeyError): + raise ValidationError({k: "id parameter not supplied"}) + + try: + item = StockItem.objects.get(pk=pk) + return item + except (ValueError, StockItem.DoesNotExist): + raise ValidationError({k, "Stock item does not exist"}) + + return None + + def getStockLocation(self): + + for k in self.data.keys(): + if k.lower() == 'stocklocation': + try: + pk = self.data[k]['id'] + except (AttributeError, KeyError): + raise ValidationError({k: "id parameter not supplied"}) + + try: + loc = StockLocation.objects.get(pk=pk) + return loc + except (ValueError, StockLocation.DoesNotExist): + raise ValidationError({k, "Stock location does not exist"}) + + return None + + def getPart(self): + + for k in self.data.keys(): + if k.lower() == 'part': + try: + pk = self.data[k]['id'] + except (AttributeError, KeyError): + raise ValidationError({k, 'id parameter not supplied'}) + + try: + part = Part.objects.get(pk=pk) + return part + except (ValueError, Part.DoesNotExist): + raise ValidationError({k, 'Part does not exist'}) + + return None diff --git a/InvenTree/barcode/tests.py b/InvenTree/barcode/tests.py new file mode 100644 index 0000000000..98e126eba1 --- /dev/null +++ b/InvenTree/barcode/tests.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +""" +Unit tests for Barcode endpoints +""" + +from django.contrib.auth import get_user_model +from django.urls import reverse + +from rest_framework.test import APITestCase +from rest_framework import status + +from stock.models import StockItem + + +class BarcodeAPITest(APITestCase): + + fixtures = [ + 'category', + 'part', + 'location', + 'stock' + ] + + def setUp(self): + # Create a user for auth + User = get_user_model() + User.objects.create_user('testuser', 'test@testing.com', 'password') + + self.client.login(username='testuser', password='password') + + self.scan_url = reverse('api-barcode-scan') + self.assign_url = reverse('api-barcode-link') + + def postBarcode(self, url, barcode): + + return self.client.post(url, format='json', data={'barcode': str(barcode)}) + + def test_invalid(self): + + response = self.client.post(self.scan_url, format='json', data={}) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_empty(self): + + response = self.postBarcode(self.scan_url, '') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.data + self.assertIn('error', data) + + self.assertIn('barcode_data', data) + self.assertIn('hash', data) + self.assertIn('plugin', data) + self.assertIsNone(data['plugin']) + + def test_barcode_generation(self): + + item = StockItem.objects.get(pk=522) + + response = self.postBarcode(self.scan_url, item.format_barcode()) + data = response.data + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertIn('stockitem', data) + + pk = data['stockitem']['pk'] + + self.assertEqual(pk, item.pk) + + def test_association(self): + """ + Test that a barcode can be associated with a StockItem + """ + + item = StockItem.objects.get(pk=522) + + self.assertEqual(len(item.uid), 0) + + barcode_data = 'A-TEST-BARCODE-STRING' + + response = self.client.post( + self.assign_url, format='json', + data={ + 'barcode': barcode_data, + 'stockitem': item.pk + } + ) + + data = response.data + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertIn('success', data) + + hash = data['hash'] + + # Read the item out from the database again + item = StockItem.objects.get(pk=522) + + self.assertEqual(hash, item.uid) + + # Ensure that the same UID cannot be assigned to a different stock item! + response = self.client.post( + self.assign_url, format='json', + data={ + 'barcode': barcode_data, + 'stockitem': 521 + } + ) + + data = response.data + + self.assertIn('error', data) + self.assertNotIn('success', data) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.mo b/InvenTree/locale/de/LC_MESSAGES/django.mo index 6cdb283c86..93b1affc81 100644 Binary files a/InvenTree/locale/de/LC_MESSAGES/django.mo and b/InvenTree/locale/de/LC_MESSAGES/django.mo differ diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 8a0f157f70..18a24b1387 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-09 06:17+0000\n" +"POT-Creation-Date: 2020-06-12 00:43+0000\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter \n" "Language-Team: C \n" @@ -17,30 +17,14 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Lokalize 19.12.0\n" -#: InvenTree/api.py:86 +#: InvenTree/api.py:83 msgid "No action specified" msgstr "Keine Aktion angegeben" -#: InvenTree/api.py:100 +#: InvenTree/api.py:97 msgid "No matching action found" msgstr "Keine passende Aktion gefunden" -#: InvenTree/api.py:131 -msgid "No barcode data provided" -msgstr "Keine Strichcodedaten bereitgestellt" - -#: InvenTree/api.py:146 -msgid "Barcode successfully decoded" -msgstr "Strichcode erfolgreich dekodiert" - -#: InvenTree/api.py:149 -msgid "Barcode plugin returned incorrect response" -msgstr "Ungültige Antwort vom Strichcode-Plugin" - -#: InvenTree/api.py:159 -msgid "Unknown barcode format" -msgstr "Unbekanntes Strichcode-Format" - #: InvenTree/forms.py:101 build/forms.py:37 msgid "Confirm" msgstr "Bestätigen" @@ -225,6 +209,50 @@ msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" msgid "Database Statistics" msgstr "Datenbankstatistiken" +#: barcode/api.py:53 barcode/api.py:150 +msgid "Must provide barcode_data parameter" +msgstr "" + +#: barcode/api.py:126 +msgid "No match found for barcode data" +msgstr "" + +#: barcode/api.py:128 +msgid "Match found for barcode data" +msgstr "" + +#: barcode/api.py:153 +msgid "Must provide stockitem parameter" +msgstr "" + +#: barcode/api.py:160 +#, fuzzy +#| msgid "No matching action found" +msgid "No matching stock item found" +msgstr "Keine passende Aktion gefunden" + +#: barcode/api.py:190 +msgid "Barcode already matches StockItem object" +msgstr "" + +#: barcode/api.py:194 +msgid "Barcode already matches StockLocation object" +msgstr "" + +#: barcode/api.py:198 +msgid "Barcode already matches Part object" +msgstr "" + +#: barcode/api.py:204 barcode/api.py:216 +msgid "Barcode hash already matches StockItem object" +msgstr "" + +#: barcode/api.py:222 +#, fuzzy +#| msgid "Create new Stock Item" +msgid "Barcode associated with StockItem" +msgstr "Neues Lagerobjekt hinzufügen" + #: build/forms.py:58 #, fuzzy #| msgid "Location Details" @@ -333,7 +361,7 @@ msgstr "Chargennummer für diese Bau-Ausgabe" #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:74 part/templates/part/part_base.html:88 -#: stock/models.py:368 stock/templates/stock/item_base.html:221 +#: stock/models.py:368 stock/templates/stock/item_base.html:230 msgid "External Link" msgstr "Externer Link" @@ -414,7 +442,7 @@ msgstr "Neues Lagerobjekt" #: build/templates/build/allocate.html:161 #: order/templates/order/sales_order_detail.html:68 #: order/templates/order/sales_order_detail.html:150 stock/models.py:362 -#: stock/templates/stock/item_base.html:180 +#: stock/templates/stock/item_base.html:189 msgid "Serial Number" msgstr "Seriennummer" @@ -431,7 +459,7 @@ msgstr "Seriennummer" #: part/templates/part/allocation.html:49 #: stock/templates/stock/item_base.html:26 #: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:186 +#: stock/templates/stock/item_base.html:195 #: stock/templates/stock/stock_adjust.html:18 templates/js/bom.html:172 #: templates/js/build.html:52 templates/js/stock.html:653 msgid "Quantity" @@ -439,7 +467,7 @@ msgstr "Anzahl" #: build/templates/build/allocate.html:177 #: build/templates/build/auto_allocate.html:20 -#: stock/templates/stock/item_base.html:162 +#: stock/templates/stock/item_base.html:171 #: stock/templates/stock/stock_adjust.html:17 templates/js/stock.html:493 msgid "Location" msgstr "Standort" @@ -521,7 +549,7 @@ msgstr "Keine Lagerobjekt gefunden, die diesem Bau zugewiesen werden können" #: build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:200 templates/js/build.html:33 +#: stock/templates/stock/item_base.html:209 templates/js/build.html:33 #: templates/navbar.html:12 msgid "Build" msgstr "Bau" @@ -541,7 +569,7 @@ msgstr "Bau-Status" #: build/templates/build/build_base.html:80 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:253 templates/js/build.html:57 +#: stock/templates/stock/item_base.html:262 templates/js/build.html:57 #: templates/js/order.html:162 templates/js/order.html:235 #: templates/js/stock.html:480 msgid "Status" @@ -553,7 +581,7 @@ msgstr "Status" #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:150 templates/js/order.html:209 +#: stock/templates/stock/item_base.html:159 templates/js/order.html:209 msgid "Sales Order" msgstr "Bestellung" @@ -622,7 +650,7 @@ msgid "Stock can be taken from any available location." msgstr "Bestand kann jedem verfügbaren Lagerort entnommen werden." #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:193 templates/js/stock.html:488 +#: stock/templates/stock/item_base.html:202 templates/js/stock.html:488 msgid "Batch" msgstr "Los" @@ -976,14 +1004,14 @@ msgstr "Hersteller" #: company/templates/company/supplier_part_detail.html:21 order/models.py:148 #: order/templates/order/order_base.html:74 #: order/templates/order/order_wizard/select_pos.html:30 -#: stock/templates/stock/item_base.html:228 templates/js/company.html:52 +#: stock/templates/stock/item_base.html:237 templates/js/company.html:52 #: templates/js/company.html:134 templates/js/order.html:144 msgid "Supplier" msgstr "Zulieferer" #: company/templates/company/detail.html:26 order/models.py:314 #: order/templates/order/sales_order_base.html:73 stock/models.py:357 -#: stock/models.py:358 stock/templates/stock/item_base.html:137 +#: stock/models.py:358 stock/templates/stock/item_base.html:146 #: templates/js/company.html:44 templates/js/order.html:217 msgid "Customer" msgstr "Kunde" @@ -1098,7 +1126,7 @@ msgstr "Neuer Auftrag" #: company/templates/company/supplier_part_base.html:6 #: company/templates/company/supplier_part_base.html:19 stock/models.py:331 -#: stock/templates/stock/item_base.html:233 templates/js/company.html:150 +#: stock/templates/stock/item_base.html:242 templates/js/company.html:150 msgid "Supplier Part" msgstr "Zulieferer-Teil" @@ -1385,7 +1413,7 @@ msgstr "Position - Notizen" #: order/models.py:466 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:23 -#: stock/templates/stock/item_base.html:207 templates/js/order.html:136 +#: stock/templates/stock/item_base.html:216 templates/js/order.html:136 msgid "Purchase Order" msgstr "Kaufvertrag" @@ -2114,14 +2142,14 @@ msgstr "Bestellung" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:215 +#: stock/templates/stock/item_base.html:224 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:106 #: templates/js/stock.html:623 msgid "Stock Item" msgstr "Lagerobjekt" #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:156 +#: stock/templates/stock/item_base.html:165 msgid "Build Order" msgstr "Bauauftrag" @@ -2539,7 +2567,7 @@ msgstr "Benutzt in" msgid "Tracking" msgstr "Tracking" -#: part/templates/part/tabs.html:64 stock/templates/stock/item_base.html:259 +#: part/templates/part/tabs.html:64 stock/templates/stock/item_base.html:268 msgid "Tests" msgstr "" @@ -2738,22 +2766,6 @@ msgstr "BOM-Position beaarbeiten" msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" -#: plugins/barcode/inventree.py:70 -msgid "Part does not exist" -msgstr "Teil existiert nicht" - -#: plugins/barcode/inventree.py:79 -msgid "StockLocation does not exist" -msgstr "Lagerort existiert nicht" - -#: plugins/barcode/inventree.py:89 -msgid "StockItem does not exist" -msgstr "Lagerobjekt existiert nicht" - -#: plugins/barcode/inventree.py:92 -msgid "No matching data" -msgstr "Keine passenden Daten" - #: report/models.py:167 #, fuzzy #| msgid "Template part" @@ -2834,7 +2846,7 @@ msgstr "Teil kann nicht zu sich selbst gehören" msgid "Parent Stock Item" msgstr "Eltern-Lagerobjekt" -#: stock/models.py:322 stock/templates/stock/item_base.html:129 +#: stock/models.py:322 stock/templates/stock/item_base.html:138 msgid "Base Part" msgstr "Basisteil" @@ -3066,99 +3078,132 @@ msgstr "" "Dieses Lagerobjekt wird automatisch gelöscht wenn der Lagerbestand " "aufgebraucht ist." -#: stock/templates/stock/item_base.html:74 +#: stock/templates/stock/item_base.html:78 #, fuzzy -#| msgid "Add stock" -msgid "Add to stock" -msgstr "Bestand hinzufügen" +#| msgid "Source Location" +msgid "Barcode actions" +msgstr "Quell-Standort" -#: stock/templates/stock/item_base.html:77 +#: stock/templates/stock/item_base.html:80 #, fuzzy -#| msgid "Remove From Stock" -msgid "Take from stock" -msgstr "Aus Lagerbestand entfernen" +#| msgid "Part QR Code" +msgid "Show QR Code" +msgstr "Teil-QR-Code" -#: stock/templates/stock/item_base.html:80 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:81 +msgid "Print Label" +msgstr "" + +#: stock/templates/stock/item_base.html:83 templates/js/barcode.html:263 +#: templates/js/barcode.html:268 +msgid "Unlink Barcode" +msgstr "" + +#: stock/templates/stock/item_base.html:85 +msgid "Link Barcode" +msgstr "" + +#: stock/templates/stock/item_base.html:92 +#, fuzzy +#| msgid "Confirm stock adjustment" +msgid "Stock adjustment actions" +msgstr "Bestands-Anpassung bestätigen" + +#: stock/templates/stock/item_base.html:95 templates/stock_table.html:14 msgid "Count stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:84 -#, fuzzy -#| msgid "Serialize Stock" -msgid "Serialize stock" -msgstr "Lagerbestand erfassen" +#: stock/templates/stock/item_base.html:96 templates/stock_table.html:12 +msgid "Add stock" +msgstr "Bestand hinzufügen" -#: stock/templates/stock/item_base.html:90 stock/views.py:232 -#, fuzzy -#| msgid "Item assigned to customer?" -msgid "Assign to Customer" -msgstr "Ist dieses Objekt einem Kunden zugeteilt?" +#: stock/templates/stock/item_base.html:97 templates/stock_table.html:13 +msgid "Remove stock" +msgstr "Bestand entfernen" -#: stock/templates/stock/item_base.html:94 +#: stock/templates/stock/item_base.html:99 #, fuzzy #| msgid "Order stock" msgid "Transfer stock" msgstr "Bestand bestellen" -#: stock/templates/stock/item_base.html:97 +#: stock/templates/stock/item_base.html:105 +#, fuzzy +#| msgid "Stock Locations" +msgid "Stock actions" +msgstr "Lagerobjekt-Standorte" + +#: stock/templates/stock/item_base.html:108 +#, fuzzy +#| msgid "Serialize Stock" +msgid "Serialize stock" +msgstr "Lagerbestand erfassen" + +#: stock/templates/stock/item_base.html:111 +#, fuzzy +#| msgid "Item assigned to customer?" +msgid "Assign to customer" +msgstr "Ist dieses Objekt einem Kunden zugeteilt?" + +#: stock/templates/stock/item_base.html:114 +#, fuzzy +#| msgid "Count stock items" +msgid "Convert to variant" +msgstr "Lagerobjekte zählen" + +#: stock/templates/stock/item_base.html:116 #, fuzzy #| msgid "Count stock items" msgid "Duplicate stock item" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:102 -#, fuzzy -#| msgid "Count stock items" -msgid "Convert stock to variant" -msgstr "Lagerobjekte zählen" - -#: stock/templates/stock/item_base.html:107 -msgid "Generate test report" -msgstr "" - -#: stock/templates/stock/item_base.html:111 +#: stock/templates/stock/item_base.html:117 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit stock item" msgstr "Lagerobjekt bearbeiten" -#: stock/templates/stock/item_base.html:115 +#: stock/templates/stock/item_base.html:119 #, fuzzy #| msgid "Delete Stock Item" msgid "Delete stock item" msgstr "Lagerobjekt löschen" #: stock/templates/stock/item_base.html:124 +msgid "Generate test report" +msgstr "" + +#: stock/templates/stock/item_base.html:133 msgid "Stock Item Details" msgstr "Lagerbestands-Details" -#: stock/templates/stock/item_base.html:144 +#: stock/templates/stock/item_base.html:153 msgid "Belongs To" msgstr "Gehört zu" -#: stock/templates/stock/item_base.html:166 +#: stock/templates/stock/item_base.html:175 #, fuzzy #| msgid "No stock location set" msgid "No location set" msgstr "Kein Lagerort gesetzt" -#: stock/templates/stock/item_base.html:173 +#: stock/templates/stock/item_base.html:182 msgid "Unique Identifier" msgstr "Eindeutiger Bezeichner" -#: stock/templates/stock/item_base.html:214 +#: stock/templates/stock/item_base.html:223 msgid "Parent Item" msgstr "Elternposition" -#: stock/templates/stock/item_base.html:239 +#: stock/templates/stock/item_base.html:248 msgid "Last Updated" msgstr "Zuletzt aktualisiert" -#: stock/templates/stock/item_base.html:244 +#: stock/templates/stock/item_base.html:253 msgid "Last Stocktake" msgstr "Letzte Inventur" -#: stock/templates/stock/item_base.html:248 +#: stock/templates/stock/item_base.html:257 msgid "No stocktake performed" msgstr "Keine Inventur ausgeführt" @@ -3289,6 +3334,12 @@ msgstr "Lagerobjekt bearbeiten" msgid "Delete Stock Item Attachment" msgstr "Teilanhang löschen" +#: stock/views.py:232 +#, fuzzy +#| msgid "Item assigned to customer?" +msgid "Assign to Customer" +msgstr "Ist dieses Objekt einem Kunden zugeteilt?" + #: stock/views.py:270 #, fuzzy #| msgid "Delete Template" @@ -3424,8 +3475,10 @@ msgid "Create new Stock Item" msgstr "Neues Lagerobjekt hinzufügen" #: stock/views.py:1167 -msgid "Copy Stock Item" -msgstr "Lagerobjekt kopieren" +#, fuzzy +#| msgid "Count stock items" +msgid "Duplicate Stock Item" +msgstr "Lagerobjekte zählen" #: stock/views.py:1240 msgid "Invalid quantity" @@ -3550,6 +3603,49 @@ msgstr "" msgid "Delete attachment" msgstr "Anhang löschen" +#: templates/js/barcode.html:28 +#, fuzzy +#| msgid "No barcode data provided" +msgid "Scan barcode data here using wedge scanner" +msgstr "Keine Strichcodedaten bereitgestellt" + +#: templates/js/barcode.html:34 +#, fuzzy +#| msgid "Source Location" +msgid "Barcode" +msgstr "Quell-Standort" + +#: templates/js/barcode.html:42 +#, fuzzy +#| msgid "No barcode data provided" +msgid "Enter barcode data" +msgstr "Keine Strichcodedaten bereitgestellt" + +#: templates/js/barcode.html:140 +#, fuzzy +#| msgid "No barcode data provided" +msgid "Scan barcode data below" +msgstr "Keine Strichcodedaten bereitgestellt" + +#: templates/js/barcode.html:195 templates/js/barcode.html:243 +#, fuzzy +#| msgid "Unknown barcode format" +msgid "Unknown response from server" +msgstr "Unbekanntes Strichcode-Format" + +#: templates/js/barcode.html:198 templates/js/barcode.html:247 +msgid "Invalid server response" +msgstr "" + +#: templates/js/barcode.html:265 +msgid "" +"This will remove the association between this stock item and the barcode" +msgstr "" + +#: templates/js/barcode.html:271 +msgid "Unlink" +msgstr "" + #: templates/js/bom.html:143 msgid "Open subassembly" msgstr "Unterbaugruppe öffnen" @@ -3693,8 +3789,10 @@ msgid "NO RESULT" msgstr "" #: templates/js/stock.html:58 +#, fuzzy +#| msgid "Edit Sales Order" msgid "Add test result" -msgstr "" +msgstr "Auftrag bearbeiten" #: templates/js/stock.html:76 #, fuzzy @@ -3832,42 +3930,38 @@ msgstr "Kaufen" msgid "Sell" msgstr "Verkaufen" -#: templates/navbar.html:36 +#: templates/navbar.html:32 +msgid "Scan Barcode" +msgstr "" + +#: templates/navbar.html:41 msgid "Admin" msgstr "Admin" -#: templates/navbar.html:39 +#: templates/navbar.html:44 msgid "Settings" msgstr "Einstellungen" -#: templates/navbar.html:40 +#: templates/navbar.html:45 msgid "Logout" msgstr "Ausloggen" -#: templates/navbar.html:42 +#: templates/navbar.html:47 msgid "Login" msgstr "Einloggen" -#: templates/navbar.html:45 +#: templates/navbar.html:50 msgid "About InvenTree" msgstr "Über InvenBaum" -#: templates/navbar.html:46 +#: templates/navbar.html:51 msgid "Statistics" msgstr "Statistiken" -#: templates/search_form.html:6 +#: templates/search_form.html:6 templates/search_form.html:8 msgid "Search" msgstr "Suche" -#: templates/stock_table.html:12 -msgid "Add stock" -msgstr "Bestand hinzufügen" - -#: templates/stock_table.html:13 -msgid "Remove stock" -msgstr "Bestand entfernen" - #: templates/stock_table.html:15 msgid "Move stock" msgstr "Bestand bewegen" @@ -3880,6 +3974,37 @@ msgstr "Bestand bestellen" msgid "Delete Stock" msgstr "Bestand löschen" +#~ msgid "Barcode successfully decoded" +#~ msgstr "Strichcode erfolgreich dekodiert" + +#~ msgid "Barcode plugin returned incorrect response" +#~ msgstr "Ungültige Antwort vom Strichcode-Plugin" + +#~ msgid "Part does not exist" +#~ msgstr "Teil existiert nicht" + +#~ msgid "StockLocation does not exist" +#~ msgstr "Lagerort existiert nicht" + +#~ msgid "StockItem does not exist" +#~ msgstr "Lagerobjekt existiert nicht" + +#~ msgid "No matching data" +#~ msgstr "Keine passenden Daten" + +#, fuzzy +#~| msgid "Add stock" +#~ msgid "Add to stock" +#~ msgstr "Bestand hinzufügen" + +#, fuzzy +#~| msgid "Remove From Stock" +#~ msgid "Take from stock" +#~ msgstr "Aus Lagerbestand entfernen" + +#~ msgid "Copy Stock Item" +#~ msgstr "Lagerobjekt kopieren" + #~ msgid "Part cannot be a variant of another part if it is already a template" #~ msgstr "" #~ "Teil kann keine Variante eines anderen Teils sein wenn es bereits eine " diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index fb77271de8..c539fbcae5 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-09 06:17+0000\n" +"POT-Creation-Date: 2020-06-12 00:43+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,30 +18,14 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: InvenTree/api.py:86 +#: InvenTree/api.py:83 msgid "No action specified" msgstr "" -#: InvenTree/api.py:100 +#: InvenTree/api.py:97 msgid "No matching action found" msgstr "" -#: InvenTree/api.py:131 -msgid "No barcode data provided" -msgstr "" - -#: InvenTree/api.py:146 -msgid "Barcode successfully decoded" -msgstr "" - -#: InvenTree/api.py:149 -msgid "Barcode plugin returned incorrect response" -msgstr "" - -#: InvenTree/api.py:159 -msgid "Unknown barcode format" -msgstr "" - #: InvenTree/forms.py:101 build/forms.py:37 msgid "Confirm" msgstr "" @@ -218,6 +202,46 @@ msgstr "" msgid "Database Statistics" msgstr "" +#: barcode/api.py:53 barcode/api.py:150 +msgid "Must provide barcode_data parameter" +msgstr "" + +#: barcode/api.py:126 +msgid "No match found for barcode data" +msgstr "" + +#: barcode/api.py:128 +msgid "Match found for barcode data" +msgstr "" + +#: barcode/api.py:153 +msgid "Must provide stockitem parameter" +msgstr "" + +#: barcode/api.py:160 +msgid "No matching stock item found" +msgstr "" + +#: barcode/api.py:190 +msgid "Barcode already matches StockItem object" +msgstr "" + +#: barcode/api.py:194 +msgid "Barcode already matches StockLocation object" +msgstr "" + +#: barcode/api.py:198 +msgid "Barcode already matches Part object" +msgstr "" + +#: barcode/api.py:204 barcode/api.py:216 +msgid "Barcode hash already matches StockItem object" +msgstr "" + +#: barcode/api.py:222 +msgid "Barcode associated with StockItem" +msgstr "" + #: build/forms.py:58 msgid "Location of completed parts" msgstr "" @@ -318,7 +342,7 @@ msgstr "" #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:74 part/templates/part/part_base.html:88 -#: stock/models.py:368 stock/templates/stock/item_base.html:221 +#: stock/models.py:368 stock/templates/stock/item_base.html:230 msgid "External Link" msgstr "" @@ -398,7 +422,7 @@ msgstr "" #: build/templates/build/allocate.html:161 #: order/templates/order/sales_order_detail.html:68 #: order/templates/order/sales_order_detail.html:150 stock/models.py:362 -#: stock/templates/stock/item_base.html:180 +#: stock/templates/stock/item_base.html:189 msgid "Serial Number" msgstr "" @@ -415,7 +439,7 @@ msgstr "" #: part/templates/part/allocation.html:49 #: stock/templates/stock/item_base.html:26 #: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:186 +#: stock/templates/stock/item_base.html:195 #: stock/templates/stock/stock_adjust.html:18 templates/js/bom.html:172 #: templates/js/build.html:52 templates/js/stock.html:653 msgid "Quantity" @@ -423,7 +447,7 @@ msgstr "" #: build/templates/build/allocate.html:177 #: build/templates/build/auto_allocate.html:20 -#: stock/templates/stock/item_base.html:162 +#: stock/templates/stock/item_base.html:171 #: stock/templates/stock/stock_adjust.html:17 templates/js/stock.html:493 msgid "Location" msgstr "" @@ -504,7 +528,7 @@ msgstr "" #: build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:200 templates/js/build.html:33 +#: stock/templates/stock/item_base.html:209 templates/js/build.html:33 #: templates/navbar.html:12 msgid "Build" msgstr "" @@ -524,7 +548,7 @@ msgstr "" #: build/templates/build/build_base.html:80 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:253 templates/js/build.html:57 +#: stock/templates/stock/item_base.html:262 templates/js/build.html:57 #: templates/js/order.html:162 templates/js/order.html:235 #: templates/js/stock.html:480 msgid "Status" @@ -536,7 +560,7 @@ msgstr "" #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:150 templates/js/order.html:209 +#: stock/templates/stock/item_base.html:159 templates/js/order.html:209 msgid "Sales Order" msgstr "" @@ -603,7 +627,7 @@ msgid "Stock can be taken from any available location." msgstr "" #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:193 templates/js/stock.html:488 +#: stock/templates/stock/item_base.html:202 templates/js/stock.html:488 msgid "Batch" msgstr "" @@ -950,14 +974,14 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:21 order/models.py:148 #: order/templates/order/order_base.html:74 #: order/templates/order/order_wizard/select_pos.html:30 -#: stock/templates/stock/item_base.html:228 templates/js/company.html:52 +#: stock/templates/stock/item_base.html:237 templates/js/company.html:52 #: templates/js/company.html:134 templates/js/order.html:144 msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 order/models.py:314 #: order/templates/order/sales_order_base.html:73 stock/models.py:357 -#: stock/models.py:358 stock/templates/stock/item_base.html:137 +#: stock/models.py:358 stock/templates/stock/item_base.html:146 #: templates/js/company.html:44 templates/js/order.html:217 msgid "Customer" msgstr "" @@ -1071,7 +1095,7 @@ msgstr "" #: company/templates/company/supplier_part_base.html:6 #: company/templates/company/supplier_part_base.html:19 stock/models.py:331 -#: stock/templates/stock/item_base.html:233 templates/js/company.html:150 +#: stock/templates/stock/item_base.html:242 templates/js/company.html:150 msgid "Supplier Part" msgstr "" @@ -1354,7 +1378,7 @@ msgstr "" #: order/models.py:466 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:23 -#: stock/templates/stock/item_base.html:207 templates/js/order.html:136 +#: stock/templates/stock/item_base.html:216 templates/js/order.html:136 msgid "Purchase Order" msgstr "" @@ -2052,14 +2076,14 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:215 +#: stock/templates/stock/item_base.html:224 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:106 #: templates/js/stock.html:623 msgid "Stock Item" msgstr "" #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:156 +#: stock/templates/stock/item_base.html:165 msgid "Build Order" msgstr "" @@ -2453,7 +2477,7 @@ msgstr "" msgid "Tracking" msgstr "" -#: part/templates/part/tabs.html:64 stock/templates/stock/item_base.html:259 +#: part/templates/part/tabs.html:64 stock/templates/stock/item_base.html:268 msgid "Tests" msgstr "" @@ -2646,22 +2670,6 @@ msgstr "" msgid "Confim BOM item deletion" msgstr "" -#: plugins/barcode/inventree.py:70 -msgid "Part does not exist" -msgstr "" - -#: plugins/barcode/inventree.py:79 -msgid "StockLocation does not exist" -msgstr "" - -#: plugins/barcode/inventree.py:89 -msgid "StockItem does not exist" -msgstr "" - -#: plugins/barcode/inventree.py:92 -msgid "No matching data" -msgstr "" - #: report/models.py:167 msgid "Template name" msgstr "" @@ -2731,7 +2739,7 @@ msgstr "" msgid "Parent Stock Item" msgstr "" -#: stock/models.py:322 stock/templates/stock/item_base.html:129 +#: stock/models.py:322 stock/templates/stock/item_base.html:138 msgid "Base Part" msgstr "" @@ -2937,79 +2945,108 @@ msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:74 -msgid "Add to stock" +#: stock/templates/stock/item_base.html:78 +msgid "Barcode actions" msgstr "" -#: stock/templates/stock/item_base.html:77 -msgid "Take from stock" +#: stock/templates/stock/item_base.html:80 +msgid "Show QR Code" msgstr "" -#: stock/templates/stock/item_base.html:80 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:81 +msgid "Print Label" +msgstr "" + +#: stock/templates/stock/item_base.html:83 templates/js/barcode.html:263 +#: templates/js/barcode.html:268 +msgid "Unlink Barcode" +msgstr "" + +#: stock/templates/stock/item_base.html:85 +msgid "Link Barcode" +msgstr "" + +#: stock/templates/stock/item_base.html:92 +msgid "Stock adjustment actions" +msgstr "" + +#: stock/templates/stock/item_base.html:95 templates/stock_table.html:14 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:84 -msgid "Serialize stock" +#: stock/templates/stock/item_base.html:96 templates/stock_table.html:12 +msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:90 stock/views.py:232 -msgid "Assign to Customer" +#: stock/templates/stock/item_base.html:97 templates/stock_table.html:13 +msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:94 +#: stock/templates/stock/item_base.html:99 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:97 -msgid "Duplicate stock item" +#: stock/templates/stock/item_base.html:105 +msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:102 -msgid "Convert stock to variant" -msgstr "" - -#: stock/templates/stock/item_base.html:107 -msgid "Generate test report" +#: stock/templates/stock/item_base.html:108 +msgid "Serialize stock" msgstr "" #: stock/templates/stock/item_base.html:111 +msgid "Assign to customer" +msgstr "" + +#: stock/templates/stock/item_base.html:114 +msgid "Convert to variant" +msgstr "" + +#: stock/templates/stock/item_base.html:116 +msgid "Duplicate stock item" +msgstr "" + +#: stock/templates/stock/item_base.html:117 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:115 +#: stock/templates/stock/item_base.html:119 msgid "Delete stock item" msgstr "" #: stock/templates/stock/item_base.html:124 +msgid "Generate test report" +msgstr "" + +#: stock/templates/stock/item_base.html:133 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:144 +#: stock/templates/stock/item_base.html:153 msgid "Belongs To" msgstr "" -#: stock/templates/stock/item_base.html:166 +#: stock/templates/stock/item_base.html:175 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:173 +#: stock/templates/stock/item_base.html:182 msgid "Unique Identifier" msgstr "" -#: stock/templates/stock/item_base.html:214 +#: stock/templates/stock/item_base.html:223 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:239 +#: stock/templates/stock/item_base.html:248 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:244 +#: stock/templates/stock/item_base.html:253 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:248 +#: stock/templates/stock/item_base.html:257 msgid "No stocktake performed" msgstr "" @@ -3128,6 +3165,10 @@ msgstr "" msgid "Delete Stock Item Attachment" msgstr "" +#: stock/views.py:232 +msgid "Assign to Customer" +msgstr "" + #: stock/views.py:270 msgid "Delete All Test Data" msgstr "" @@ -3251,7 +3292,7 @@ msgid "Create new Stock Item" msgstr "" #: stock/views.py:1167 -msgid "Copy Stock Item" +msgid "Duplicate Stock Item" msgstr "" #: stock/views.py:1240 @@ -3375,6 +3416,39 @@ msgstr "" msgid "Delete attachment" msgstr "" +#: templates/js/barcode.html:28 +msgid "Scan barcode data here using wedge scanner" +msgstr "" + +#: templates/js/barcode.html:34 +msgid "Barcode" +msgstr "" + +#: templates/js/barcode.html:42 +msgid "Enter barcode data" +msgstr "" + +#: templates/js/barcode.html:140 +msgid "Scan barcode data below" +msgstr "" + +#: templates/js/barcode.html:195 templates/js/barcode.html:243 +msgid "Unknown response from server" +msgstr "" + +#: templates/js/barcode.html:198 templates/js/barcode.html:247 +msgid "Invalid server response" +msgstr "" + +#: templates/js/barcode.html:265 +msgid "" +"This will remove the association between this stock item and the barcode" +msgstr "" + +#: templates/js/barcode.html:271 +msgid "Unlink" +msgstr "" + #: templates/js/bom.html:143 msgid "Open subassembly" msgstr "" @@ -3635,42 +3709,38 @@ msgstr "" msgid "Sell" msgstr "" -#: templates/navbar.html:36 +#: templates/navbar.html:32 +msgid "Scan Barcode" +msgstr "" + +#: templates/navbar.html:41 msgid "Admin" msgstr "" -#: templates/navbar.html:39 +#: templates/navbar.html:44 msgid "Settings" msgstr "" -#: templates/navbar.html:40 +#: templates/navbar.html:45 msgid "Logout" msgstr "" -#: templates/navbar.html:42 +#: templates/navbar.html:47 msgid "Login" msgstr "" -#: templates/navbar.html:45 +#: templates/navbar.html:50 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:46 +#: templates/navbar.html:51 msgid "Statistics" msgstr "" -#: templates/search_form.html:6 +#: templates/search_form.html:6 templates/search_form.html:8 msgid "Search" msgstr "" -#: templates/stock_table.html:12 -msgid "Add stock" -msgstr "" - -#: templates/stock_table.html:13 -msgid "Remove stock" -msgstr "" - #: templates/stock_table.html:15 msgid "Move stock" msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index fb77271de8..c539fbcae5 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-09 06:17+0000\n" +"POT-Creation-Date: 2020-06-12 00:43+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,30 +18,14 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: InvenTree/api.py:86 +#: InvenTree/api.py:83 msgid "No action specified" msgstr "" -#: InvenTree/api.py:100 +#: InvenTree/api.py:97 msgid "No matching action found" msgstr "" -#: InvenTree/api.py:131 -msgid "No barcode data provided" -msgstr "" - -#: InvenTree/api.py:146 -msgid "Barcode successfully decoded" -msgstr "" - -#: InvenTree/api.py:149 -msgid "Barcode plugin returned incorrect response" -msgstr "" - -#: InvenTree/api.py:159 -msgid "Unknown barcode format" -msgstr "" - #: InvenTree/forms.py:101 build/forms.py:37 msgid "Confirm" msgstr "" @@ -218,6 +202,46 @@ msgstr "" msgid "Database Statistics" msgstr "" +#: barcode/api.py:53 barcode/api.py:150 +msgid "Must provide barcode_data parameter" +msgstr "" + +#: barcode/api.py:126 +msgid "No match found for barcode data" +msgstr "" + +#: barcode/api.py:128 +msgid "Match found for barcode data" +msgstr "" + +#: barcode/api.py:153 +msgid "Must provide stockitem parameter" +msgstr "" + +#: barcode/api.py:160 +msgid "No matching stock item found" +msgstr "" + +#: barcode/api.py:190 +msgid "Barcode already matches StockItem object" +msgstr "" + +#: barcode/api.py:194 +msgid "Barcode already matches StockLocation object" +msgstr "" + +#: barcode/api.py:198 +msgid "Barcode already matches Part object" +msgstr "" + +#: barcode/api.py:204 barcode/api.py:216 +msgid "Barcode hash already matches StockItem object" +msgstr "" + +#: barcode/api.py:222 +msgid "Barcode associated with StockItem" +msgstr "" + #: build/forms.py:58 msgid "Location of completed parts" msgstr "" @@ -318,7 +342,7 @@ msgstr "" #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:74 part/templates/part/part_base.html:88 -#: stock/models.py:368 stock/templates/stock/item_base.html:221 +#: stock/models.py:368 stock/templates/stock/item_base.html:230 msgid "External Link" msgstr "" @@ -398,7 +422,7 @@ msgstr "" #: build/templates/build/allocate.html:161 #: order/templates/order/sales_order_detail.html:68 #: order/templates/order/sales_order_detail.html:150 stock/models.py:362 -#: stock/templates/stock/item_base.html:180 +#: stock/templates/stock/item_base.html:189 msgid "Serial Number" msgstr "" @@ -415,7 +439,7 @@ msgstr "" #: part/templates/part/allocation.html:49 #: stock/templates/stock/item_base.html:26 #: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:186 +#: stock/templates/stock/item_base.html:195 #: stock/templates/stock/stock_adjust.html:18 templates/js/bom.html:172 #: templates/js/build.html:52 templates/js/stock.html:653 msgid "Quantity" @@ -423,7 +447,7 @@ msgstr "" #: build/templates/build/allocate.html:177 #: build/templates/build/auto_allocate.html:20 -#: stock/templates/stock/item_base.html:162 +#: stock/templates/stock/item_base.html:171 #: stock/templates/stock/stock_adjust.html:17 templates/js/stock.html:493 msgid "Location" msgstr "" @@ -504,7 +528,7 @@ msgstr "" #: build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:200 templates/js/build.html:33 +#: stock/templates/stock/item_base.html:209 templates/js/build.html:33 #: templates/navbar.html:12 msgid "Build" msgstr "" @@ -524,7 +548,7 @@ msgstr "" #: build/templates/build/build_base.html:80 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:253 templates/js/build.html:57 +#: stock/templates/stock/item_base.html:262 templates/js/build.html:57 #: templates/js/order.html:162 templates/js/order.html:235 #: templates/js/stock.html:480 msgid "Status" @@ -536,7 +560,7 @@ msgstr "" #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:150 templates/js/order.html:209 +#: stock/templates/stock/item_base.html:159 templates/js/order.html:209 msgid "Sales Order" msgstr "" @@ -603,7 +627,7 @@ msgid "Stock can be taken from any available location." msgstr "" #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:193 templates/js/stock.html:488 +#: stock/templates/stock/item_base.html:202 templates/js/stock.html:488 msgid "Batch" msgstr "" @@ -950,14 +974,14 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:21 order/models.py:148 #: order/templates/order/order_base.html:74 #: order/templates/order/order_wizard/select_pos.html:30 -#: stock/templates/stock/item_base.html:228 templates/js/company.html:52 +#: stock/templates/stock/item_base.html:237 templates/js/company.html:52 #: templates/js/company.html:134 templates/js/order.html:144 msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 order/models.py:314 #: order/templates/order/sales_order_base.html:73 stock/models.py:357 -#: stock/models.py:358 stock/templates/stock/item_base.html:137 +#: stock/models.py:358 stock/templates/stock/item_base.html:146 #: templates/js/company.html:44 templates/js/order.html:217 msgid "Customer" msgstr "" @@ -1071,7 +1095,7 @@ msgstr "" #: company/templates/company/supplier_part_base.html:6 #: company/templates/company/supplier_part_base.html:19 stock/models.py:331 -#: stock/templates/stock/item_base.html:233 templates/js/company.html:150 +#: stock/templates/stock/item_base.html:242 templates/js/company.html:150 msgid "Supplier Part" msgstr "" @@ -1354,7 +1378,7 @@ msgstr "" #: order/models.py:466 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:23 -#: stock/templates/stock/item_base.html:207 templates/js/order.html:136 +#: stock/templates/stock/item_base.html:216 templates/js/order.html:136 msgid "Purchase Order" msgstr "" @@ -2052,14 +2076,14 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:215 +#: stock/templates/stock/item_base.html:224 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:106 #: templates/js/stock.html:623 msgid "Stock Item" msgstr "" #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:156 +#: stock/templates/stock/item_base.html:165 msgid "Build Order" msgstr "" @@ -2453,7 +2477,7 @@ msgstr "" msgid "Tracking" msgstr "" -#: part/templates/part/tabs.html:64 stock/templates/stock/item_base.html:259 +#: part/templates/part/tabs.html:64 stock/templates/stock/item_base.html:268 msgid "Tests" msgstr "" @@ -2646,22 +2670,6 @@ msgstr "" msgid "Confim BOM item deletion" msgstr "" -#: plugins/barcode/inventree.py:70 -msgid "Part does not exist" -msgstr "" - -#: plugins/barcode/inventree.py:79 -msgid "StockLocation does not exist" -msgstr "" - -#: plugins/barcode/inventree.py:89 -msgid "StockItem does not exist" -msgstr "" - -#: plugins/barcode/inventree.py:92 -msgid "No matching data" -msgstr "" - #: report/models.py:167 msgid "Template name" msgstr "" @@ -2731,7 +2739,7 @@ msgstr "" msgid "Parent Stock Item" msgstr "" -#: stock/models.py:322 stock/templates/stock/item_base.html:129 +#: stock/models.py:322 stock/templates/stock/item_base.html:138 msgid "Base Part" msgstr "" @@ -2937,79 +2945,108 @@ msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:74 -msgid "Add to stock" +#: stock/templates/stock/item_base.html:78 +msgid "Barcode actions" msgstr "" -#: stock/templates/stock/item_base.html:77 -msgid "Take from stock" +#: stock/templates/stock/item_base.html:80 +msgid "Show QR Code" msgstr "" -#: stock/templates/stock/item_base.html:80 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:81 +msgid "Print Label" +msgstr "" + +#: stock/templates/stock/item_base.html:83 templates/js/barcode.html:263 +#: templates/js/barcode.html:268 +msgid "Unlink Barcode" +msgstr "" + +#: stock/templates/stock/item_base.html:85 +msgid "Link Barcode" +msgstr "" + +#: stock/templates/stock/item_base.html:92 +msgid "Stock adjustment actions" +msgstr "" + +#: stock/templates/stock/item_base.html:95 templates/stock_table.html:14 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:84 -msgid "Serialize stock" +#: stock/templates/stock/item_base.html:96 templates/stock_table.html:12 +msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:90 stock/views.py:232 -msgid "Assign to Customer" +#: stock/templates/stock/item_base.html:97 templates/stock_table.html:13 +msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:94 +#: stock/templates/stock/item_base.html:99 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:97 -msgid "Duplicate stock item" +#: stock/templates/stock/item_base.html:105 +msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:102 -msgid "Convert stock to variant" -msgstr "" - -#: stock/templates/stock/item_base.html:107 -msgid "Generate test report" +#: stock/templates/stock/item_base.html:108 +msgid "Serialize stock" msgstr "" #: stock/templates/stock/item_base.html:111 +msgid "Assign to customer" +msgstr "" + +#: stock/templates/stock/item_base.html:114 +msgid "Convert to variant" +msgstr "" + +#: stock/templates/stock/item_base.html:116 +msgid "Duplicate stock item" +msgstr "" + +#: stock/templates/stock/item_base.html:117 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:115 +#: stock/templates/stock/item_base.html:119 msgid "Delete stock item" msgstr "" #: stock/templates/stock/item_base.html:124 +msgid "Generate test report" +msgstr "" + +#: stock/templates/stock/item_base.html:133 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:144 +#: stock/templates/stock/item_base.html:153 msgid "Belongs To" msgstr "" -#: stock/templates/stock/item_base.html:166 +#: stock/templates/stock/item_base.html:175 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:173 +#: stock/templates/stock/item_base.html:182 msgid "Unique Identifier" msgstr "" -#: stock/templates/stock/item_base.html:214 +#: stock/templates/stock/item_base.html:223 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:239 +#: stock/templates/stock/item_base.html:248 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:244 +#: stock/templates/stock/item_base.html:253 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:248 +#: stock/templates/stock/item_base.html:257 msgid "No stocktake performed" msgstr "" @@ -3128,6 +3165,10 @@ msgstr "" msgid "Delete Stock Item Attachment" msgstr "" +#: stock/views.py:232 +msgid "Assign to Customer" +msgstr "" + #: stock/views.py:270 msgid "Delete All Test Data" msgstr "" @@ -3251,7 +3292,7 @@ msgid "Create new Stock Item" msgstr "" #: stock/views.py:1167 -msgid "Copy Stock Item" +msgid "Duplicate Stock Item" msgstr "" #: stock/views.py:1240 @@ -3375,6 +3416,39 @@ msgstr "" msgid "Delete attachment" msgstr "" +#: templates/js/barcode.html:28 +msgid "Scan barcode data here using wedge scanner" +msgstr "" + +#: templates/js/barcode.html:34 +msgid "Barcode" +msgstr "" + +#: templates/js/barcode.html:42 +msgid "Enter barcode data" +msgstr "" + +#: templates/js/barcode.html:140 +msgid "Scan barcode data below" +msgstr "" + +#: templates/js/barcode.html:195 templates/js/barcode.html:243 +msgid "Unknown response from server" +msgstr "" + +#: templates/js/barcode.html:198 templates/js/barcode.html:247 +msgid "Invalid server response" +msgstr "" + +#: templates/js/barcode.html:265 +msgid "" +"This will remove the association between this stock item and the barcode" +msgstr "" + +#: templates/js/barcode.html:271 +msgid "Unlink" +msgstr "" + #: templates/js/bom.html:143 msgid "Open subassembly" msgstr "" @@ -3635,42 +3709,38 @@ msgstr "" msgid "Sell" msgstr "" -#: templates/navbar.html:36 +#: templates/navbar.html:32 +msgid "Scan Barcode" +msgstr "" + +#: templates/navbar.html:41 msgid "Admin" msgstr "" -#: templates/navbar.html:39 +#: templates/navbar.html:44 msgid "Settings" msgstr "" -#: templates/navbar.html:40 +#: templates/navbar.html:45 msgid "Logout" msgstr "" -#: templates/navbar.html:42 +#: templates/navbar.html:47 msgid "Login" msgstr "" -#: templates/navbar.html:45 +#: templates/navbar.html:50 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:46 +#: templates/navbar.html:51 msgid "Statistics" msgstr "" -#: templates/search_form.html:6 +#: templates/search_form.html:6 templates/search_form.html:8 msgid "Search" msgstr "" -#: templates/stock_table.html:12 -msgid "Add stock" -msgstr "" - -#: templates/stock_table.html:13 -msgid "Remove stock" -msgstr "" - #: templates/stock_table.html:15 msgid "Move stock" msgstr "" diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index d9f80edf44..5cdeccea13 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -35,44 +35,42 @@ {% endif %}

{{ part.description }}

-

-

-
- - {% if part.is_template == False %} - {% include "qr_button.html" %} - {% if part.active %} - - {% if not part.virtual %} - - {% endif %} - {% if part.purchaseable %} - - {% endif %} - {% endif %} - {% endif %} - - - {% if not part.active %} - - {% endif %} -
+
+ + + -

+ {% if part.active %} + + + {% if part.purchaseable %} + + {% endif %} + {% endif %} + +
{% if part.IPN %} diff --git a/InvenTree/plugins/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py deleted file mode 100644 index 94a6c34df4..0000000000 --- a/InvenTree/plugins/barcode/barcode.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- - -import hashlib - -from stock.serializers import StockItemSerializer, LocationSerializer -from part.serializers import PartSerializer - -import plugins.plugin as plugin - - -class BarcodePlugin(plugin.InvenTreePlugin): - """ - The BarcodePlugin class is the base class for any barcode plugin. - """ - - def __init__(self, barcode_data): - plugin.InvenTreePlugin.__init__(self) - - self.data = barcode_data - - def hash(self): - """ - Calculate a hash for the barcode data. - This is supposed to uniquely identify the barcode contents, - at least within the bardcode sub-type. - - The default implementation simply returns an MD5 hash of the barcode data, - encoded to a string. - - This may be sufficient for most applications, but can obviously be overridden - by a subclass. - - """ - - hash = hashlib.md5(str(self.data).encode()) - return str(hash.hexdigest()) - - def validate(self): - """ - Default implementation returns False - """ - return False - - def decode(self): - """ - Decode the barcode, and craft a response - """ - - return None - - def render_part(self, part): - """ - Render a Part object to JSON - Use the existing serializer to do this. - """ - - serializer = PartSerializer(part) - - return serializer.data - - def render_stock_location(self, loc): - """ - Render a StockLocation object to JSON - Use the existing serializer to do this. - """ - - serializer = LocationSerializer(loc) - - return serializer.data - - def render_stock_item(self, item): - """ - Render a StockItem object to JSON. - Use the existing serializer to do this - """ - - serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True) - - return serializer.data diff --git a/InvenTree/plugins/barcode/digikey.py b/InvenTree/plugins/barcode/digikey.py deleted file mode 100644 index 2542fe964a..0000000000 --- a/InvenTree/plugins/barcode/digikey.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import barcode - - -class DigikeyBarcodePlugin(barcode.BarcodePlugin): - - PLUGIN_NAME = "DigikeyBarcodePlugin" diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py deleted file mode 100644 index 93b86d42b7..0000000000 --- a/InvenTree/plugins/barcode/inventree.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -The InvenTreeBarcodePlugin validates barcodes generated by InvenTree itself. -It can be used as a template for developing third-party barcode plugins. - -The data format is very simple, and maps directly to database objects, -via the "id" parameter. - -Parsing an InvenTree barcode simply involves validating that the -references model objects actually exist in the database. -""" - -# -*- coding: utf-8 -*- - -import json - -from . import barcode - -from stock.models import StockItem, StockLocation -from part.models import Part - -from django.utils.translation import ugettext as _ - - -class InvenTreeBarcodePlugin(barcode.BarcodePlugin): - - PLUGIN_NAME = "InvenTreeBarcodePlugin" - - def validate(self): - """ - An "InvenTree" barcode must include the following tags: - - { - 'tool': 'InvenTree', - 'version': - } - - """ - - # The data must either be dict or be able to dictified - if type(self.data) is dict: - pass - elif type(self.data) is str: - try: - self.data = json.loads(self.data) - except json.JSONDecodeError: - return False - else: - return False - - for key in ['tool', 'version']: - if key not in self.data.keys(): - return False - - if not self.data['tool'] == 'InvenTree': - return False - - return True - - def decode(self): - - response = {} - - if 'part' in self.data.keys(): - id = self.data['part'].get('id', None) - - try: - part = Part.objects.get(id=id) - response['part'] = self.render_part(part) - except (ValueError, Part.DoesNotExist): - response['error'] = _('Part does not exist') - - elif 'stocklocation' in self.data.keys(): - id = self.data['stocklocation'].get('id', None) - - try: - loc = StockLocation.objects.get(id=id) - response['stocklocation'] = self.render_stock_location(loc) - except (ValueError, StockLocation.DoesNotExist): - response['error'] = _('StockLocation does not exist') - - elif 'stockitem' in self.data.keys(): - - id = self.data['stockitem'].get('id', None) - - try: - item = StockItem.objects.get(id=id) - response['stockitem'] = self.render_stock_item(item) - except (ValueError, StockItem.DoesNotExist): - response['error'] = _('StockItem does not exist') - - else: - response['error'] = _('No matching data') - - return response diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index f913c1f295..ae6e0630e5 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -4,10 +4,6 @@ import inspect import importlib import pkgutil -# Barcode plugins -import plugins.barcode as barcode -from plugins.barcode.barcode import BarcodePlugin - # Action plugins import plugins.action as action from plugins.action.action import ActionPlugin @@ -51,24 +47,6 @@ def get_plugins(pkg, baseclass): return plugins -def load_barcode_plugins(): - """ - Return a list of all registered barcode plugins - """ - - print("Loading barcode plugins") - - plugins = get_plugins(barcode, BarcodePlugin) - - if len(plugins) > 0: - print("Discovered {n} barcode plugins:".format(n=len(plugins))) - - for bp in plugins: - print(" - {bp}".format(bp=bp.PLUGIN_NAME)) - - return plugins - - def load_action_plugins(): """ Return a list of all registered action plugins diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 399da8d2b8..63804291ec 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -67,55 +67,64 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} -
- {% include "qr_button.html" %} +
+ + +
+ +
+ + {% if item.in_stock %} - {% if not item.serialized %} - - - - {% if item.part.trackable %} - - {% endif %} - {% endif %} - {% if item.part.salable %} - - {% endif %} - - - {% endif %} - {% if item.part.has_variants %} - + + {% endif %} + + {% if item.part.has_test_report_templates %} {% endif %} - - {% if item.can_delete %} - - {% endif %}
{% endblock %} @@ -131,6 +140,19 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {{ item.part.full_name }} + {% if item.serialized %} +
+ + + + + {% else %} + + + + + + {% endif %} {% if item.customer %} @@ -174,19 +196,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} - {% if item.serialized %} - - - - - - {% else %} - - - - - - {% endif %} {% if item.batch %} @@ -330,6 +339,14 @@ $("#show-qr-code").click(function() { }); }); +$("#link-barcode").click(function() { + linkBarcodeDialog({{ item.id }}); +}); + +$("#unlink-barcode").click(function() { + unlinkBarcode({{ item.id }}); +}); + {% if item.in_stock %} {% if item.part.salable %} @@ -385,10 +402,11 @@ $('#stock-add').click(function() { $("#stock-delete").click(function () { launchModalForm( - "{% url 'stock-item-delete' item.id %}", - { - redirect: "{% url 'part-stock' item.part.id %}" - }); + "{% url 'stock-item-delete' item.id %}", + { + redirect: "{% url 'part-stock' item.part.id %}" + } + ); }); {% endblock %} diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index f92539cab8..20032e557c 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -12,25 +12,36 @@

{% trans "Stock" %}

{% trans "All stock items" %}

{% endif %} -

-

{% if location %} @@ -109,6 +120,12 @@ inventreeDel('show-part-locs'); }); + {% if location %} + $("#barcode-check-in").click(function() { + barcodeCheckIn({{ location.id }}); + }); + {% endif %} + $("#stock-export").click(function() { launchModalForm("{% url 'stock-export-options' %}", { submit_text: "Export", diff --git a/InvenTree/stock/templates/stock/location_delete.html b/InvenTree/stock/templates/stock/location_delete.html index ef5cee7541..68e34b987b 100644 --- a/InvenTree/stock/templates/stock/location_delete.html +++ b/InvenTree/stock/templates/stock/location_delete.html @@ -1,7 +1,10 @@ {% extends "modal_delete_form.html" %} +{% load i18n %} +{% load inventree_extras %} + {% block pre_form_content %} -Are you sure you want to delete stock location '{{ location.name }}'? +{% trans "Are you sure you want to delete this stock location?" %}
@@ -33,7 +36,7 @@ If this location is deleted, these items will be moved to the top level 'Stock'
    {% for item in location.stock_items.all %} -
  • {{ item.part.full_name }} - {{ item.part.description }}{{ item.quantity }}
  • +
  • {{ item.part.full_name }} - {{ item.part.description }}{% decimal item.quantity %}
  • {% endfor %}
{% endif %} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 6f9eeeec2e..49db90d578 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1164,7 +1164,7 @@ class StockItemCreate(AjaxCreateView): try: original = StockItem.objects.get(pk=item_to_copy) initials = model_to_dict(original) - self.ajax_form_title = _("Copy Stock Item") + self.ajax_form_title = _("Duplicate Stock Item") except StockItem.DoesNotExist: initials = super(StockItemCreate, self).get_initial().copy() diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 63487eb625..1c869a8ca4 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -108,6 +108,7 @@ InvenTree + @@ -132,6 +133,11 @@ $(document).ready(function () { inventreeDocReady(); showCachedAlerts(); + + $('#barcode-scan').click(function() { + barcodeScanDialog(); + }); + }); diff --git a/InvenTree/templates/js/barcode.html b/InvenTree/templates/js/barcode.html new file mode 100644 index 0000000000..9c9a8c1b08 --- /dev/null +++ b/InvenTree/templates/js/barcode.html @@ -0,0 +1,514 @@ +{% load i18n %} + +function makeBarcodeInput(placeholderText='') { + /* + * Generate HTML for a barcode input + */ + + placeholderText = placeholderText || '{% trans "Scan barcode data here using wedge scanner" %}'; + + var html = ` +
+ +
+
+ + + + +
+
{% trans "Enter barcode data" %}
+
+
+ `; + + return html; +} + + +function showBarcodeMessage(modal, message, style='danger') { + + var html = `
`; + + html += message; + + html += "
"; + + $(modal + ' #barcode-error-message').html(html); +} + + +function showInvalidResponseError(modal, response, status) { + showBarcodeMessage(modal, `{% trans "Invalid server response" %}
{% trans "Status" %}: '${status}'`); +} + + +function clearBarcodeError(modal, message) { + + $(modal + ' #barcode-error-message').html(''); +} + + +function enableBarcodeInput(modal, enabled=true) { + + var barcode = $(modal + ' #barcode'); + + barcode.prop('disabled', !enabled); + + modalEnable(modal, enabled); + + barcode.focus(); +} + +function getBarcodeData(modal) { + + modal = modal || '#modal-form'; + + var el = $(modal + ' #barcode'); + + var barcode = el.val(); + + el.val(''); + el.focus(); + + return barcode.trim(); +} + + +function barcodeDialog(title, options={}) { + /* + * Handle a barcode display dialog. + */ + + var modal = '#modal-form'; + + function sendBarcode() { + var barcode = getBarcodeData(modal); + + if (barcode && barcode.length > 0) { + + if (options.onScan) { + options.onScan(barcode); + } + } + } + + $(modal).on('shown.bs.modal', function() { + $(modal + ' .modal-form-content').scrollTop(0); + + var barcode = $(modal + ' #barcode'); + + // Handle 'enter' key on barcode + barcode.keyup(function(event) { + event.preventDefault(); + + if (event.which == 10 || event.which == 13) { + sendBarcode(); + } + }); + + // Ensure the barcode field has focus + barcode.focus(); + + var form = $(modal).find('.js-modal-form'); + + // Override form submission + form.submit(function() { + return false; + }); + + // Callback for when the "submit" button is pressed on the modal + modalSubmit(modal, function() { + if (options.onSubmit) { + options.onSubmit(); + } + }); + + if (options.onShow) { + options.onShow(); + } + + }); + + modalSetTitle(modal, title); + + if (options.onSubmit) { + modalShowSubmitButton(modal, true); + } else { + modalShowSubmitButton(modal, false); + } + + var content = ''; + + content += `
{% trans "Scan barcode data below" %}
`; + + content += `
`; + content += `
`; + + // Optional content before barcode input + content += `
`; + content += options.headerContent || ''; + content += `
`; + + content += makeBarcodeInput(); + + if (options.extraFields) { + content += options.extraFields; + } + + content += ``; + + // Optional content after barcode input + content += `'; + + modalSetContent(modal, content); + + $(modal).modal({ + backdrop: 'static', + keyboard: false, + }); + + if (options.preShow) { + options.preShow(); + } + + $(modal).modal('show'); +} + + +function barcodeScanDialog() { + /* + * Perform a barcode scan, + * and (potentially) redirect the browser + */ + + var modal = '#modal-form'; + + 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) { + 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'); + } else { + showBarcodeMessage(modal, "{% trans 'Unknown response from server' %}", 'warning'); + } + } else { + showInvalidResponseError(modal, response, status); + } + }, + }, + ); + }, + }, + ); +} + + +/* + * Dialog for linking a particular barcode to a stock item. + */ +function linkBarcodeDialog(stockitem, options={}) { + + var modal = '#modal-form'; + + barcodeDialog( + "{% trans 'Link Barcode to Stock Item' %}", + { + onScan: function(barcode) { + enableBarcodeInput(modal, false); + inventreePut( + '/api/barcode/link/', + { + barcode: barcode, + stockitem: stockitem, + }, + { + method: 'POST', + success: function(response, status) { + + 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); + } + }, + }, + ); + } + } + ); +} + + +/* + * Remove barcode association from a device. + */ +function unlinkBarcode(stockitem) { + + var html = `{% trans "Unlink Barcode" %}
`; + + html += "{% trans 'This will remove the association between this stock item and the barcode' %}"; + + showQuestionDialog( + "{% trans 'Unlink Barcode' %}", + html, + { + accept_text: "{% trans 'Unlink' %}", + accept: function() { + inventreePut( + `/api/stock/${stockitem}/`, + { + // Clear the UID field + uid: '', + }, + { + method: 'PATCH', + success: function(response, status) { + location.reload(); + }, + }, + ); + }, + } + ); +} + + +/* + * Display dialog to check multiple stock items in to a stock location. + */ +function barcodeCheckIn(location_id, options={}) { + + var modal = '#modal-form'; + + // List of items we are going to checkin + var items = []; + + + function reloadTable() { + + modalEnable(modal, false); + + // Remove click listeners + $(modal + ' .button-item-remove').unbind('click'); + + var table = $(modal + ' #items-table-div'); + + var html = ` +
{% trans "Serial Number" %}{{ item.serial }}
{% trans "Quantity" %}{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}
{{ item.uid }}
{% trans "Serial Number" %}{{ item.serial }}
{% trans "Quantity" %}{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}
+ + + + + + + + + `; + + items.forEach(function(item) { + html += ` + + + + + + `; + }); + + html += ` + +
{% trans "Part" %}{% trans "Location" %}{% trans "Quantity" %}
${imageHoverIcon(item.part_detail.thumbnail)} ${item.part_detail.name}${item.location_detail.name}${item.quantity}${makeIconButton('fa-times-circle icon-red', 'button-item-remove', item.pk, '{% trans "Remove stock item" %}')}
`; + + table.html(html); + + modalEnable(modal, items.length > 0); + + $(modal + ' #barcode').focus(); + + $(modal + ' .button-item-remove').unbind('click').on('mouseup', function() { + var pk = $(this).attr('pk'); + + var match = false; + + for (var ii = 0; ii < items.length; ii++) { + if (pk.toString() == items[ii].pk.toString()) { + items.splice(ii, 1); + match = true; + break; + } + } + + if (match) { + reloadTable(); + } + + return false; + + }); + } + + var table = `
`; + + // Extra form fields + var extra = ` +
+ +
+
+ + + + +
+
{% trans "Enter optional notes for stock transfer" %}
+
+
`; + + barcodeDialog( + "{% trans "Check Stock Items into Location" %}", + { + headerContent: table, + preShow: function() { + modalSetSubmitText(modal, '{% trans "Check In" %}'); + modalEnable(modal, false); + reloadTable(); + }, + onShow: function() { + }, + extraFields: extra, + onSubmit: function() { + + + // Called when the 'check-in' button is pressed + + var data = {location: location_id}; + + // Extract 'notes' field + data.notes = $(modal + ' #notes').val(); + + var entries = []; + + items.forEach(function(item) { + entries.push({ + pk: item.pk, + quantity: item.quantity, + }); + }); + + data.items = entries; + + inventreePut( + '{% url 'api-stock-transfer' %}', + data, + { + method: 'POST', + success: function(response, status) { + // Hide the modal + $(modal).modal('hide'); + if (status == 'success' && 'success' in response) { + + showAlertOrCache('alert-success', response.success, true); + location.reload(); + } else { + showAlertOrCache('alert-success', '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') { + if ('stockitem' in response) { + stockitem = response.stockitem; + + var duplicate = false; + + items.forEach(function(item) { + if (item.pk == stockitem.pk) { + duplicate = true; + } + }); + + if (duplicate) { + showBarcodeMessage(modal, "{% trans "Stock Item already scanned" %}", "warning"); + } else { + + if (stockitem.location == location_id) { + 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"); + + reloadTable(); + } + + } else { + // Barcode does not match a stock item + showBarcodeMessage(modal, "{% trans "Barcode does not match Stock Item" %}", "warning"); + } + } else { + showInvalidResponseError(modal, response, status); + } + }, + }, + ); + }, + } + ); +} diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 7bccb6d62d..0a231044cd 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -28,6 +28,11 @@