mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	@@ -12,7 +12,7 @@ from decimal import Decimal
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from wsgiref.util import FileWrapper
 | 
					from wsgiref.util import FileWrapper
 | 
				
			||||||
from django.http import StreamingHttpResponse
 | 
					from django.http import StreamingHttpResponse
 | 
				
			||||||
from django.core.exceptions import ValidationError
 | 
					from django.core.exceptions import ValidationError, FieldError
 | 
				
			||||||
from django.utils.translation import ugettext as _
 | 
					from django.utils.translation import ugettext as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.models import Permission
 | 
					from django.contrib.auth.models import Permission
 | 
				
			||||||
@@ -414,7 +414,7 @@ def extract_serial_numbers(serials, expected_quantity):
 | 
				
			|||||||
    return numbers
 | 
					    return numbers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def validateFilterString(value):
 | 
					def validateFilterString(value, model=None):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Validate that a provided filter string looks like a list of comma-separated key=value pairs
 | 
					    Validate that a provided filter string looks like a list of comma-separated key=value pairs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -464,6 +464,15 @@ def validateFilterString(value):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        results[k] = v
 | 
					        results[k] = v
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # If a model is provided, verify that the provided filters can be used against it
 | 
				
			||||||
 | 
					    if model is not None:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            model.objects.filter(**results)
 | 
				
			||||||
 | 
					        except FieldError as e:
 | 
				
			||||||
 | 
					            raise ValidationError(
 | 
				
			||||||
 | 
					                str(e),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return results
 | 
					    return results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,7 @@ from company.api import company_api_urls
 | 
				
			|||||||
from stock.api import stock_api_urls
 | 
					from stock.api import stock_api_urls
 | 
				
			||||||
from build.api import build_api_urls
 | 
					from build.api import build_api_urls
 | 
				
			||||||
from order.api import order_api_urls
 | 
					from order.api import order_api_urls
 | 
				
			||||||
 | 
					from label.api import label_api_urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.conf.urls.static import static
 | 
					from django.conf.urls.static import static
 | 
				
			||||||
@@ -58,6 +59,7 @@ apipatterns = [
 | 
				
			|||||||
    url(r'^stock/', include(stock_api_urls)),
 | 
					    url(r'^stock/', include(stock_api_urls)),
 | 
				
			||||||
    url(r'^build/', include(build_api_urls)),
 | 
					    url(r'^build/', include(build_api_urls)),
 | 
				
			||||||
    url(r'^order/', include(order_api_urls)),
 | 
					    url(r'^order/', include(order_api_urls)),
 | 
				
			||||||
 | 
					    url(r'^label/', include(label_api_urls)),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # User URLs
 | 
					    # User URLs
 | 
				
			||||||
    url(r'^user/', include(user_urls)),
 | 
					    url(r'^user/', include(user_urls)),
 | 
				
			||||||
@@ -90,6 +92,7 @@ settings_urls = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer
 | 
					# Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer
 | 
				
			||||||
dynamic_javascript_urls = [
 | 
					dynamic_javascript_urls = [
 | 
				
			||||||
 | 
					    url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'),
 | 
				
			||||||
    url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'),
 | 
					    url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'),
 | 
				
			||||||
    url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'),
 | 
					    url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'),
 | 
				
			||||||
    url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'),
 | 
					    url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'),
 | 
				
			||||||
@@ -97,6 +100,7 @@ dynamic_javascript_urls = [
 | 
				
			|||||||
    url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
 | 
					    url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
 | 
				
			||||||
    url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'),
 | 
					    url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'),
 | 
				
			||||||
    url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'),
 | 
					    url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'),
 | 
				
			||||||
 | 
					    url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
 | 
				
			||||||
    url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
 | 
					    url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
 | 
				
			||||||
    url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
 | 
					    url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,12 +3,13 @@ from __future__ import unicode_literals
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import StockItemLabel
 | 
					from .models import StockItemLabel, StockLocationLabel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockItemLabelAdmin(admin.ModelAdmin):
 | 
					class LabelAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    list_display = ('name', 'description', 'label', 'filters', 'enabled')
 | 
					    list_display = ('name', 'description', 'label', 'filters', 'enabled')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
admin.site.register(StockItemLabel, StockItemLabelAdmin)
 | 
					admin.site.register(StockItemLabel, LabelAdmin)
 | 
				
			||||||
 | 
					admin.site.register(StockLocationLabel, LabelAdmin)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										375
									
								
								InvenTree/label/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								InvenTree/label/api.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,375 @@
 | 
				
			|||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.utils.translation import ugettext as _
 | 
				
			||||||
 | 
					from django.conf.urls import url, include
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rest_framework import generics, filters
 | 
				
			||||||
 | 
					from rest_framework.response import Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import InvenTree.helpers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from stock.models import StockItem, StockLocation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import StockItemLabel, StockLocationLabel
 | 
				
			||||||
 | 
					from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LabelListView(generics.ListAPIView):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Generic API class for label templates
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filter_backends = [
 | 
				
			||||||
 | 
					        DjangoFilterBackend,
 | 
				
			||||||
 | 
					        filters.SearchFilter
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filter_fields = [
 | 
				
			||||||
 | 
					        'enabled',
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    search_fields = [
 | 
				
			||||||
 | 
					        'name',
 | 
				
			||||||
 | 
					        'description',
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockItemLabelMixin:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Mixin for extracting stock items from query params
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_items(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return a list of requested stock items
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        items = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        params = self.request.query_params
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'items[]' in params:
 | 
				
			||||||
 | 
					            items = params.getlist('items[]', [])
 | 
				
			||||||
 | 
					        elif 'item' in params:
 | 
				
			||||||
 | 
					            items = [params.get('item', None)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if type(items) not in [list, tuple]:
 | 
				
			||||||
 | 
					            items = [items]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        valid_ids = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for item in items:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                valid_ids.append(int(item))
 | 
				
			||||||
 | 
					            except (ValueError):
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # List of StockItems which match provided values
 | 
				
			||||||
 | 
					        valid_items = StockItem.objects.filter(pk__in=valid_ids)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return valid_items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockItemLabelList(LabelListView, StockItemLabelMixin):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    API endpoint for viewing list of StockItemLabel objects.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Filterable by:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - enabled: Filter by enabled / disabled status
 | 
				
			||||||
 | 
					    - item: Filter by single stock item
 | 
				
			||||||
 | 
					    - items: Filter by list of stock items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = StockItemLabel.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = StockItemLabelSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def filter_queryset(self, queryset):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Filter the StockItem label queryset.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        queryset = super().filter_queryset(queryset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # List of StockItem objects to match against
 | 
				
			||||||
 | 
					        items = self.get_items()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # We wish to filter by stock items
 | 
				
			||||||
 | 
					        if len(items) > 0:
 | 
				
			||||||
 | 
					            """
 | 
				
			||||||
 | 
					            At this point, we are basically forced to be inefficient,
 | 
				
			||||||
 | 
					            as we need to compare the 'filters' string of each label,
 | 
				
			||||||
 | 
					            and see if it matches against each of the requested items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            TODO: In the future, if this becomes excessively slow, it
 | 
				
			||||||
 | 
					                  will need to be readdressed.
 | 
				
			||||||
 | 
					            """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Keep track of which labels match every specified stockitem
 | 
				
			||||||
 | 
					            valid_label_ids = set()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for label in queryset.all():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                matches = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Filter string defined for the StockItemLabel object
 | 
				
			||||||
 | 
					                filters = InvenTree.helpers.validateFilterString(label.filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for item in items:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    item_query = StockItem.objects.filter(pk=item.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if not item_query.filter(**filters).exists():
 | 
				
			||||||
 | 
					                        matches = False
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Matched all items
 | 
				
			||||||
 | 
					                if matches:
 | 
				
			||||||
 | 
					                    valid_label_ids.add(label.pk)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Reduce queryset to only valid matches
 | 
				
			||||||
 | 
					            queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return queryset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    API endpoint for a single StockItemLabel object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = StockItemLabel.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = StockItemLabelSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    API endpoint for printing a StockItemLabel object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = StockItemLabel.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = StockItemLabelSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Check if valid stock item(s) have been provided.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        items = self.get_items()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if len(items) == 0:
 | 
				
			||||||
 | 
					            # No valid items provided, return an error message
 | 
				
			||||||
 | 
					            data = {
 | 
				
			||||||
 | 
					                'error': _('Must provide valid StockItem(s)'),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Response(data, status=400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label = self.get_object()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            pdf = label.render(items)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            e = sys.exc_info()[1]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            data = {
 | 
				
			||||||
 | 
					                'error': _('Error during label rendering'),
 | 
				
			||||||
 | 
					                'message': str(e),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Response(data, status=400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return InvenTree.helpers.DownloadFile(
 | 
				
			||||||
 | 
					            pdf.getbuffer(),
 | 
				
			||||||
 | 
					            'stock_item_label.pdf',
 | 
				
			||||||
 | 
					            content_type='application/pdf'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockLocationLabelMixin:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Mixin for extracting stock locations from query params
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_locations(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return a list of requested stock locations
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        locations = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        params = self.request.query_params
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'locations[]' in params:
 | 
				
			||||||
 | 
					            locations = params.getlist('locations[]', [])
 | 
				
			||||||
 | 
					        elif 'location' in params:
 | 
				
			||||||
 | 
					            locations = [params.get('location', None)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if type(locations) not in [list, tuple]:
 | 
				
			||||||
 | 
					            locations = [locations]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        valid_ids = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for loc in locations:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                valid_ids.append(int(loc))
 | 
				
			||||||
 | 
					            except (ValueError):
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # List of StockLocation objects which match provided values
 | 
				
			||||||
 | 
					        valid_locations = StockLocation.objects.filter(pk__in=valid_ids)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return valid_locations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    API endpoint for viewiing list of StockLocationLabel objects.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Filterable by:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - enabled: Filter by enabled / disabled status
 | 
				
			||||||
 | 
					    - location: Filter by a single stock location
 | 
				
			||||||
 | 
					    - locations: Filter by list of stock locations
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = StockLocationLabel.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = StockLocationLabelSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def filter_queryset(self, queryset):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Filter the StockLocationLabel queryset
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        queryset = super().filter_queryset(queryset)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # List of StockLocation objects to match against
 | 
				
			||||||
 | 
					        locations = self.get_locations()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # We wish to filter by stock location(s)
 | 
				
			||||||
 | 
					        if len(locations) > 0:
 | 
				
			||||||
 | 
					            """
 | 
				
			||||||
 | 
					            At this point, we are basically forced to be inefficient,
 | 
				
			||||||
 | 
					            as we need to compare the 'filters' string of each label,
 | 
				
			||||||
 | 
					            and see if it matches against each of the requested items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            TODO: In the future, if this becomes excessively slow, it
 | 
				
			||||||
 | 
					                  will need to be readdressed.
 | 
				
			||||||
 | 
					            """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            valid_label_ids = set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for label in queryset.all():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                matches = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Filter string defined for the StockLocationLabel object
 | 
				
			||||||
 | 
					                filters = InvenTree.helpers.validateFilterString(label.filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for loc in locations:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    loc_query = StockLocation.objects.filter(pk=loc.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if not loc_query.filter(**filters).exists():
 | 
				
			||||||
 | 
					                        matches = False
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Matched all items
 | 
				
			||||||
 | 
					                if matches:
 | 
				
			||||||
 | 
					                    valid_label_ids.add(label.pk)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Reduce queryset to only valid matches
 | 
				
			||||||
 | 
					            queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return queryset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    API endpoint for a single StockLocationLabel object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = StockLocationLabel.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = StockLocationLabelSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    API endpoint for printing a StockLocationLabel object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = StockLocationLabel.objects.all()
 | 
				
			||||||
 | 
					    seiralizers_class = StockLocationLabelSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        locations = self.get_locations()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if len(locations) == 0:
 | 
				
			||||||
 | 
					            # No valid locations provided - return an error message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Response(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    'error': _('Must provide valid StockLocation(s)'),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                status=400,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label = self.get_object()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            pdf = label.render(locations)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            e = sys.exc_info()[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            data = {
 | 
				
			||||||
 | 
					                'error': _('Error during label rendering'),
 | 
				
			||||||
 | 
					                'message': str(e),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Response(data, status=400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return InvenTree.helpers.DownloadFile(
 | 
				
			||||||
 | 
					            pdf.getbuffer(),
 | 
				
			||||||
 | 
					            'stock_location_label.pdf',
 | 
				
			||||||
 | 
					            content_type='application/pdf'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					label_api_urls = [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Stock item labels
 | 
				
			||||||
 | 
					    url(r'stock/', include([
 | 
				
			||||||
 | 
					        # Detail views
 | 
				
			||||||
 | 
					        url(r'^(?P<pk>\d+)/', include([
 | 
				
			||||||
 | 
					            url(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'),
 | 
				
			||||||
 | 
					            url(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'),
 | 
				
			||||||
 | 
					        ])),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # List view
 | 
				
			||||||
 | 
					        url(r'^.*$', StockItemLabelList.as_view(), name='api-stockitem-label-list'),
 | 
				
			||||||
 | 
					    ])),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Stock location labels
 | 
				
			||||||
 | 
					    url(r'location/', include([
 | 
				
			||||||
 | 
					        # Detail views
 | 
				
			||||||
 | 
					        url(r'^(?P<pk>\d+)/', include([
 | 
				
			||||||
 | 
					            url(r'print/?', StockLocationLabelPrint.as_view(), name='api-stocklocation-label-print'),
 | 
				
			||||||
 | 
					            url(r'^.*$', StockLocationLabelDetail.as_view(), name='api-stocklocation-label-detail'),
 | 
				
			||||||
 | 
					        ])),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # List view
 | 
				
			||||||
 | 
					        url(r'^.*$', StockLocationLabelList.as_view(), name='api-stocklocation-label-list'),
 | 
				
			||||||
 | 
					    ])),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
@@ -1,5 +1,168 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import AppConfig
 | 
					from django.apps import AppConfig
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LabelConfig(AppConfig):
 | 
					class LabelConfig(AppConfig):
 | 
				
			||||||
    name = 'label'
 | 
					    name = 'label'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ready(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This function is called whenever the label app is loaded
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.create_stock_item_labels()
 | 
				
			||||||
 | 
					        self.create_stock_location_labels()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create_stock_item_labels(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Create database entries for the default StockItemLabel templates,
 | 
				
			||||||
 | 
					        if they do not already exist
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            from .models import StockItemLabel
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            # Database might not by ready yet
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        src_dir = os.path.join(
 | 
				
			||||||
 | 
					            os.path.dirname(os.path.realpath(__file__)),
 | 
				
			||||||
 | 
					            'templates',
 | 
				
			||||||
 | 
					            'stockitem',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dst_dir = os.path.join(
 | 
				
			||||||
 | 
					            settings.MEDIA_ROOT,
 | 
				
			||||||
 | 
					            'label',
 | 
				
			||||||
 | 
					            'inventree',
 | 
				
			||||||
 | 
					            'stockitem',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not os.path.exists(dst_dir):
 | 
				
			||||||
 | 
					            logger.info(f"Creating missing directory: '{dst_dir}'")
 | 
				
			||||||
 | 
					            os.makedirs(dst_dir, exist_ok=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        labels = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'file': 'qr.html',
 | 
				
			||||||
 | 
					                'name': 'QR Code',
 | 
				
			||||||
 | 
					                'description': 'Simple QR code label',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for label in labels:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            filename = os.path.join(
 | 
				
			||||||
 | 
					                'label',
 | 
				
			||||||
 | 
					                'inventree',
 | 
				
			||||||
 | 
					                'stockitem',
 | 
				
			||||||
 | 
					                label['file'],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Check if the file exists in the media directory
 | 
				
			||||||
 | 
					            src_file = os.path.join(src_dir, label['file'])
 | 
				
			||||||
 | 
					            dst_file = os.path.join(settings.MEDIA_ROOT, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if not os.path.exists(dst_file):
 | 
				
			||||||
 | 
					                logger.info(f"Copying label template '{dst_file}'")
 | 
				
			||||||
 | 
					                shutil.copyfile(src_file, dst_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                # Check if a label matching the template already exists
 | 
				
			||||||
 | 
					                if StockItemLabel.objects.filter(label=filename).exists():
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                logger.info(f"Creating entry for StockItemLabel '{label['name']}'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                StockItemLabel.objects.create(
 | 
				
			||||||
 | 
					                    name=label['name'],
 | 
				
			||||||
 | 
					                    description=label['description'],
 | 
				
			||||||
 | 
					                    label=filename,
 | 
				
			||||||
 | 
					                    filters='',
 | 
				
			||||||
 | 
					                    enabled=True
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create_stock_location_labels(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Create database entries for the default StockItemLocation templates,
 | 
				
			||||||
 | 
					        if they do not already exist
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            from .models import StockLocationLabel
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            # Database might not yet be ready
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        src_dir = os.path.join(
 | 
				
			||||||
 | 
					            os.path.dirname(os.path.realpath(__file__)),
 | 
				
			||||||
 | 
					            'templates',
 | 
				
			||||||
 | 
					            'stocklocation',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dst_dir = os.path.join(
 | 
				
			||||||
 | 
					            settings.MEDIA_ROOT,
 | 
				
			||||||
 | 
					            'label',
 | 
				
			||||||
 | 
					            'inventree',
 | 
				
			||||||
 | 
					            'stocklocation',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not os.path.exists(dst_dir):
 | 
				
			||||||
 | 
					            logger.info(f"Creating missing directory: '{dst_dir}'")
 | 
				
			||||||
 | 
					            os.makedirs(dst_dir, exist_ok=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        labels = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'file': 'qr.html',
 | 
				
			||||||
 | 
					                'name': 'QR Code',
 | 
				
			||||||
 | 
					                'description': 'Simple QR code label',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'file': 'qr_and_text.html',
 | 
				
			||||||
 | 
					                'name': 'QR and text',
 | 
				
			||||||
 | 
					                'description': 'Label with QR code and name of location',
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for label in labels:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            filename = os.path.join(
 | 
				
			||||||
 | 
					                'label',
 | 
				
			||||||
 | 
					                'inventree',
 | 
				
			||||||
 | 
					                'stocklocation',
 | 
				
			||||||
 | 
					                label['file'],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Check if the file exists in the media directory
 | 
				
			||||||
 | 
					            src_file = os.path.join(src_dir, label['file'])
 | 
				
			||||||
 | 
					            dst_file = os.path.join(settings.MEDIA_ROOT, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if not os.path.exists(dst_file):
 | 
				
			||||||
 | 
					                logger.info(f"Copying label template '{dst_file}'")
 | 
				
			||||||
 | 
					                shutil.copyfile(src_file, dst_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                # Check if a label matching the template already exists
 | 
				
			||||||
 | 
					                if StockLocationLabel.objects.filter(label=filename).exists():
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                logger.info(f"Creating entry for StockLocationLabel '{label['name']}'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                StockLocationLabel.objects.create(
 | 
				
			||||||
 | 
					                    name=label['name'],
 | 
				
			||||||
 | 
					                    description=label['description'],
 | 
				
			||||||
 | 
					                    label=filename,
 | 
				
			||||||
 | 
					                    filters='',
 | 
				
			||||||
 | 
					                    enabled=True
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								InvenTree/label/migrations/0003_stocklocationlabel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								InvenTree/label/migrations/0003_stocklocationlabel.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.0.7 on 2021-01-08 12:06
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import InvenTree.helpers
 | 
				
			||||||
 | 
					import django.core.validators
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					import label.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('label', '0002_stockitemlabel_enabled'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='StockLocationLabel',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(help_text='Label name', max_length=100, unique=True)),
 | 
				
			||||||
 | 
					                ('description', models.CharField(blank=True, help_text='Label description', max_length=250, null=True)),
 | 
				
			||||||
 | 
					                ('label', models.FileField(help_text='Label template file', upload_to=label.models.rename_label, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])])),
 | 
				
			||||||
 | 
					                ('filters', models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs', max_length=250, validators=[InvenTree.helpers.validateFilterString])),
 | 
				
			||||||
 | 
					                ('enabled', models.BooleanField(default=True, help_text='Label template is enabled', verbose_name='Enabled')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'abstract': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										56
									
								
								InvenTree/label/migrations/0004_auto_20210111_2302.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								InvenTree/label/migrations/0004_auto_20210111_2302.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.0.7 on 2021-01-11 12:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import InvenTree.helpers
 | 
				
			||||||
 | 
					import django.core.validators
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					import label.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('label', '0003_stocklocationlabel'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stockitemlabel',
 | 
				
			||||||
 | 
					            name='description',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='Label description', max_length=250, null=True, verbose_name='Description'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stockitemlabel',
 | 
				
			||||||
 | 
					            name='filters',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs', max_length=250, validators=[InvenTree.helpers.validateFilterString], verbose_name='Filters'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stockitemlabel',
 | 
				
			||||||
 | 
					            name='label',
 | 
				
			||||||
 | 
					            field=models.FileField(help_text='Label template file', unique=True, upload_to=label.models.rename_label, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])], verbose_name='Label'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stockitemlabel',
 | 
				
			||||||
 | 
					            name='name',
 | 
				
			||||||
 | 
					            field=models.CharField(help_text='Label name', max_length=100, verbose_name='Name'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stocklocationlabel',
 | 
				
			||||||
 | 
					            name='description',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='Label description', max_length=250, null=True, verbose_name='Description'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stocklocationlabel',
 | 
				
			||||||
 | 
					            name='filters',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs', max_length=250, validators=[InvenTree.helpers.validateFilterString], verbose_name='Filters'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stocklocationlabel',
 | 
				
			||||||
 | 
					            name='label',
 | 
				
			||||||
 | 
					            field=models.FileField(help_text='Label template file', unique=True, upload_to=label.models.rename_label, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])], verbose_name='Label'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stocklocationlabel',
 | 
				
			||||||
 | 
					            name='name',
 | 
				
			||||||
 | 
					            field=models.CharField(help_text='Label name', max_length=100, verbose_name='Name'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										24
									
								
								InvenTree/label/migrations/0005_auto_20210113_2302.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								InvenTree/label/migrations/0005_auto_20210113_2302.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.0.7 on 2021-01-13 12:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					import label.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('label', '0004_auto_20210111_2302'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stockitemlabel',
 | 
				
			||||||
 | 
					            name='filters',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs', max_length=250, validators=[label.models.validate_stock_item_filters], verbose_name='Filters'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stocklocationlabel',
 | 
				
			||||||
 | 
					            name='filters',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs', max_length=250, validators=[label.models.validate_stock_location_filters], verbose_name='Filters'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -17,7 +17,7 @@ from django.utils.translation import gettext_lazy as _
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from InvenTree.helpers import validateFilterString, normalize
 | 
					from InvenTree.helpers import validateFilterString, normalize
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from stock.models import StockItem
 | 
					import stock.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def rename_label(instance, filename):
 | 
					def rename_label(instance, filename):
 | 
				
			||||||
@@ -28,6 +28,20 @@ def rename_label(instance, filename):
 | 
				
			|||||||
    return os.path.join('label', 'template', instance.SUBDIR, filename)
 | 
					    return os.path.join('label', 'template', instance.SUBDIR, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def validate_stock_item_filters(filters):
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    filters = validateFilterString(filters, model=stock.models.StockItem)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def validate_stock_location_filters(filters):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filters = validateFilterString(filters, model=stock.models.StockLocation)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LabelTemplate(models.Model):
 | 
					class LabelTemplate(models.Model):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Base class for generic, filterable labels.
 | 
					    Base class for generic, filterable labels.
 | 
				
			||||||
@@ -50,30 +64,31 @@ class LabelTemplate(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(
 | 
					    name = models.CharField(
 | 
				
			||||||
        unique=True,
 | 
					 | 
				
			||||||
        blank=False, max_length=100,
 | 
					        blank=False, max_length=100,
 | 
				
			||||||
 | 
					        verbose_name=_('Name'),
 | 
				
			||||||
        help_text=_('Label name'),
 | 
					        help_text=_('Label name'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    description = models.CharField(max_length=250, help_text=_('Label description'), blank=True, null=True)
 | 
					    description = models.CharField(
 | 
				
			||||||
 | 
					        max_length=250,
 | 
				
			||||||
 | 
					        blank=True, null=True,
 | 
				
			||||||
 | 
					        verbose_name=_('Description'),
 | 
				
			||||||
 | 
					        help_text=_('Label description'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    label = models.FileField(
 | 
					    label = models.FileField(
 | 
				
			||||||
        upload_to=rename_label,
 | 
					        upload_to=rename_label,
 | 
				
			||||||
 | 
					        unique=True,
 | 
				
			||||||
        blank=False, null=False,
 | 
					        blank=False, null=False,
 | 
				
			||||||
 | 
					        verbose_name=_('Label'),
 | 
				
			||||||
        help_text=_('Label template file'),
 | 
					        help_text=_('Label template file'),
 | 
				
			||||||
        validators=[FileExtensionValidator(allowed_extensions=['html'])],
 | 
					        validators=[FileExtensionValidator(allowed_extensions=['html'])],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    filters = models.CharField(
 | 
					 | 
				
			||||||
        blank=True, max_length=250,
 | 
					 | 
				
			||||||
        help_text=_('Query filters (comma-separated list of key=value pairs'),
 | 
					 | 
				
			||||||
        validators=[validateFilterString]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    enabled = models.BooleanField(
 | 
					    enabled = models.BooleanField(
 | 
				
			||||||
        default=True,
 | 
					        default=True,
 | 
				
			||||||
 | 
					        verbose_name=_('Enabled'),
 | 
				
			||||||
        help_text=_('Label template is enabled'),
 | 
					        help_text=_('Label template is enabled'),
 | 
				
			||||||
        verbose_name=_('Enabled')
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_record_data(self, items):
 | 
					    def get_record_data(self, items):
 | 
				
			||||||
@@ -117,6 +132,14 @@ class StockItemLabel(LabelTemplate):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    SUBDIR = "stockitem"
 | 
					    SUBDIR = "stockitem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filters = models.CharField(
 | 
				
			||||||
 | 
					        blank=True, max_length=250,
 | 
				
			||||||
 | 
					        help_text=_('Query filters (comma-separated list of key=value pairs'),
 | 
				
			||||||
 | 
					        verbose_name=_('Filters'),
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            validate_stock_item_filters]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def matches_stock_item(self, item):
 | 
					    def matches_stock_item(self, item):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Test if this label template matches a given StockItem object
 | 
					        Test if this label template matches a given StockItem object
 | 
				
			||||||
@@ -124,7 +147,7 @@ class StockItemLabel(LabelTemplate):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        filters = validateFilterString(self.filters)
 | 
					        filters = validateFilterString(self.filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        items = StockItem.objects.filter(**filters)
 | 
					        items = stock.models.StockItem.objects.filter(**filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        items = items.filter(pk=item.pk)
 | 
					        items = items.filter(pk=item.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,3 +176,47 @@ class StockItemLabel(LabelTemplate):
 | 
				
			|||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return records
 | 
					        return records
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockLocationLabel(LabelTemplate):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Template for printing StockLocation labels
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SUBDIR = "stocklocation"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filters = models.CharField(
 | 
				
			||||||
 | 
					        blank=True, max_length=250,
 | 
				
			||||||
 | 
					        help_text=_('Query filters (comma-separated list of key=value pairs'),
 | 
				
			||||||
 | 
					        verbose_name=_('Filters'),
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            validate_stock_location_filters]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def matches_stock_location(self, location):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test if this label template matches a given StockLocation object
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filters = validateFilterString(self.filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        locs = stock.models.StockLocation.objects.filter(**filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        locs = locs.filter(pk=location.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return locs.exists()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_record_data(self, locations):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Generate context data for each provided StockLocation
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        records = []
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for loc in locations:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            records.append({
 | 
				
			||||||
 | 
					                'location': loc,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return records
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										45
									
								
								InvenTree/label/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								InvenTree/label/serializers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from InvenTree.serializers import InvenTreeModelSerializer
 | 
				
			||||||
 | 
					from InvenTree.serializers import InvenTreeAttachmentSerializerField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import StockItemLabel, StockLocationLabel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockItemLabelSerializer(InvenTreeModelSerializer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Serializes a StockItemLabel object.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    label = InvenTreeAttachmentSerializerField(required=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = StockItemLabel
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            'pk',
 | 
				
			||||||
 | 
					            'name',
 | 
				
			||||||
 | 
					            'description',
 | 
				
			||||||
 | 
					            'label',
 | 
				
			||||||
 | 
					            'filters',
 | 
				
			||||||
 | 
					            'enabled',
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockLocationLabelSerializer(InvenTreeModelSerializer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Serializes a StockLocationLabel object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    label = InvenTreeAttachmentSerializerField(required=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = StockLocationLabel
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            'pk',
 | 
				
			||||||
 | 
					            'name',
 | 
				
			||||||
 | 
					            'description',
 | 
				
			||||||
 | 
					            'label',
 | 
				
			||||||
 | 
					            'filters',
 | 
				
			||||||
 | 
					            'enabled',
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
							
								
								
									
										16
									
								
								InvenTree/label/templates/stockitem/qr.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								InvenTree/label/templates/stockitem/qr.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<style>
 | 
				
			||||||
 | 
					@page {
 | 
				
			||||||
 | 
					    width: 24mm;
 | 
				
			||||||
 | 
					    height: 24mm;
 | 
				
			||||||
 | 
					    padding: 1mm;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.qr {
 | 
				
			||||||
 | 
					    margin: 2px;
 | 
				
			||||||
 | 
					    width: 22mm;
 | 
				
			||||||
 | 
					    height: 22mm;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<img class='qr' src="{{ label_tools.qr_code(item.barcode) }}"/>
 | 
				
			||||||
							
								
								
									
										16
									
								
								InvenTree/label/templates/stocklocation/qr.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								InvenTree/label/templates/stocklocation/qr.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<style>
 | 
				
			||||||
 | 
					@page {
 | 
				
			||||||
 | 
					    width: 24mm;
 | 
				
			||||||
 | 
					    height: 24mm;
 | 
				
			||||||
 | 
					    padding: 1mm;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.qr {
 | 
				
			||||||
 | 
					    margin: 2px;
 | 
				
			||||||
 | 
					    width: 22mm;
 | 
				
			||||||
 | 
					    height: 22mm;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<img class='qr' src="{{ label_tools.qr_code(location.barcode) }}"/>
 | 
				
			||||||
							
								
								
									
										43
									
								
								InvenTree/label/templates/stocklocation/qr_and_text.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								InvenTree/label/templates/stocklocation/qr_and_text.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					<style>
 | 
				
			||||||
 | 
					    @page {
 | 
				
			||||||
 | 
					        width: 75mm;
 | 
				
			||||||
 | 
					        height: 24mm;
 | 
				
			||||||
 | 
					        padding: 1mm;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    .location {
 | 
				
			||||||
 | 
					        padding: 5px;
 | 
				
			||||||
 | 
					        font-weight: bold;
 | 
				
			||||||
 | 
					        font-family: Arial, Helvetica, sans-serif;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        vertical-align: middle;
 | 
				
			||||||
 | 
					        float: right;
 | 
				
			||||||
 | 
					        display: inline;
 | 
				
			||||||
 | 
					        font-size: 125%;
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        top: 0mm;
 | 
				
			||||||
 | 
					        left: 23mm;
 | 
				
			||||||
 | 
					        white-space: nowrap;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					        text-overflow: ellipsis;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    .qr {
 | 
				
			||||||
 | 
					        margin: 2px;
 | 
				
			||||||
 | 
					        width: 22mm;
 | 
				
			||||||
 | 
					        height: 22mm;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <img class='qr' src="{{ label_tools.qr_code(location.barcode) }}"/>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <div class='location'>
 | 
				
			||||||
 | 
					        {{ location.name }}
 | 
				
			||||||
 | 
					        <br>
 | 
				
			||||||
 | 
					        <br>
 | 
				
			||||||
 | 
					        <hr>
 | 
				
			||||||
 | 
					        Location ID: {{ location.pk }}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
@@ -1 +1,76 @@
 | 
				
			|||||||
# Create your tests here.
 | 
					# Tests for Part Parameters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from InvenTree.helpers import validateFilterString
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import StockItemLabel, StockLocationLabel
 | 
				
			||||||
 | 
					from stock.models import StockItem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LabelTest(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO - Implement this test properly. Looks like apps.py is not run first
 | 
				
			||||||
 | 
					    def _test_default_labels(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test that the default label templates are copied across
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        labels = StockItemLabel.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertTrue(labels.count() > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        labels = StockLocationLabel.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertTrue(labels.count() > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO - Implement this test properly. Looks like apps.py is not run first
 | 
				
			||||||
 | 
					    def _test_default_files(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test that label files exist in the MEDIA directory
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        item_dir = os.path.join(
 | 
				
			||||||
 | 
					            settings.MEDIA_ROOT,
 | 
				
			||||||
 | 
					            'label',
 | 
				
			||||||
 | 
					            'inventree',
 | 
				
			||||||
 | 
					            'stockitem',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        files = os.listdir(item_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertTrue(len(files) > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        loc_dir = os.path.join(
 | 
				
			||||||
 | 
					            settings.MEDIA_ROOT,
 | 
				
			||||||
 | 
					            'label',
 | 
				
			||||||
 | 
					            'inventree',
 | 
				
			||||||
 | 
					            'stocklocation',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        files = os.listdir(loc_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertTrue(len(files) > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_filters(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test the label filters
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filter_string = "part__pk=10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filters = validateFilterString(filter_string, model=StockItem)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(type(filters), dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bad_filter_string = "part_pk=10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with self.assertRaises(ValidationError):
 | 
				
			||||||
 | 
					            validateFilterString(bad_filter_string, model=StockItem)
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												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
											
										
									
								
							@@ -32,6 +32,7 @@ from InvenTree import helpers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import common.models
 | 
					import common.models
 | 
				
			||||||
import report.models
 | 
					import report.models
 | 
				
			||||||
 | 
					import label.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from InvenTree.status_codes import StockStatus
 | 
					from InvenTree.status_codes import StockStatus
 | 
				
			||||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
 | 
					from InvenTree.models import InvenTreeTree, InvenTreeAttachment
 | 
				
			||||||
@@ -63,6 +64,13 @@ class StockLocation(InvenTreeTree):
 | 
				
			|||||||
            **kwargs
 | 
					            **kwargs
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def barcode(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Brief payload data (e.g. for labels)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.format_barcode(brief=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_stock_items(self, cascade=True):
 | 
					    def get_stock_items(self, cascade=True):
 | 
				
			||||||
        """ Return a queryset for all stock items under this category.
 | 
					        """ Return a queryset for all stock items under this category.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -330,6 +338,13 @@ class StockItem(MPTTModel):
 | 
				
			|||||||
            **kwargs
 | 
					            **kwargs
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def barcode(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Brief payload data (e.g. for labels)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.format_barcode(brief=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
 | 
					    uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    parent = TreeForeignKey(
 | 
					    parent = TreeForeignKey(
 | 
				
			||||||
@@ -1333,14 +1348,31 @@ class StockItem(MPTTModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return len(self.available_test_reports()) > 0
 | 
					        return len(self.available_test_reports()) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def available_labels(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return a list of Label objects which match this StockItem
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        labels = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        item_query = StockItem.objects.filter(pk=self.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for lbl in label.models.StockItemLabel.objects.filter(enabled=True):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            filters = helpers.validateFilterString(lbl.filters)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if item_query.filter(**filters).exists():
 | 
				
			||||||
 | 
					                labels.append(lbl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return labels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def has_labels(self):
 | 
					    def has_labels(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return True if there are any label templates available for this stock item
 | 
					        Return True if there are any label templates available for this stock item
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # TODO - Implement this
 | 
					        return len(self.available_labels()) > 0
 | 
				
			||||||
        return True
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
 | 
					@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -403,12 +403,7 @@ $("#stock-test-report").click(function() {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#print-label").click(function() {
 | 
					$("#print-label").click(function() {
 | 
				
			||||||
    launchModalForm(
 | 
					    printStockItemLabels([{{ item.pk }}]);
 | 
				
			||||||
        "{% url 'stock-item-label-select' item.id %}",
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            follow: true,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#stock-duplicate").click(function() {
 | 
					$("#stock-duplicate").click(function() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@
 | 
				
			|||||||
            <button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
 | 
					            <button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
 | 
				
			||||||
            <ul class='dropdown-menu' role='menu'>
 | 
					            <ul class='dropdown-menu' role='menu'>
 | 
				
			||||||
                <li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
 | 
					                <li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
 | 
				
			||||||
                <li class='disabled'><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
 | 
					                <li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
 | 
				
			||||||
                <li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
 | 
					                <li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
 | 
				
			||||||
            </ul>
 | 
					            </ul>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -205,6 +205,15 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    $('#print-label').click(function() {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					        var locs = [{{ location.pk }}];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        printStockLocationLabels(locs);        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('#show-qr-code').click(function() {
 | 
					    $('#show-qr-code').click(function() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,6 @@ stock_item_detail_urls = [
 | 
				
			|||||||
    url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
 | 
					    url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    url(r'^test-report-select/', views.StockItemTestReportSelect.as_view(), name='stock-item-test-report-select'),
 | 
					    url(r'^test-report-select/', views.StockItemTestReportSelect.as_view(), name='stock-item-test-report-select'),
 | 
				
			||||||
    url(r'^label-select/', views.StockItemSelectLabels.as_view(), name='stock-item-label-select'),
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    url(r'^test/', views.StockItemDetail.as_view(template_name='stock/item_tests.html'), name='stock-item-test-results'),
 | 
					    url(r'^test/', views.StockItemDetail.as_view(template_name='stock/item_tests.html'), name='stock-item-test-results'),
 | 
				
			||||||
    url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'),
 | 
					    url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'),
 | 
				
			||||||
@@ -64,7 +63,6 @@ stock_urls = [
 | 
				
			|||||||
    url(r'^item/uninstall/', views.StockItemUninstall.as_view(), name='stock-item-uninstall'),
 | 
					    url(r'^item/uninstall/', views.StockItemUninstall.as_view(), name='stock-item-uninstall'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    url(r'^item/test-report-download/', views.StockItemTestReportDownload.as_view(), name='stock-item-test-report-download'),
 | 
					    url(r'^item/test-report-download/', views.StockItemTestReportDownload.as_view(), name='stock-item-test-report-download'),
 | 
				
			||||||
    url(r'^item/print-stock-labels/', views.StockItemPrintLabels.as_view(), name='stock-item-print-labels'),
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # URLs for StockItem attachments
 | 
					    # URLs for StockItem attachments
 | 
				
			||||||
    url(r'^item/attachment/', include([
 | 
					    url(r'^item/attachment/', include([
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,7 +31,6 @@ from datetime import datetime, timedelta
 | 
				
			|||||||
from company.models import Company, SupplierPart
 | 
					from company.models import Company, SupplierPart
 | 
				
			||||||
from part.models import Part
 | 
					from part.models import Part
 | 
				
			||||||
from report.models import TestReport
 | 
					from report.models import TestReport
 | 
				
			||||||
from label.models import StockItemLabel
 | 
					 | 
				
			||||||
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
 | 
					from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import common.settings
 | 
					import common.settings
 | 
				
			||||||
@@ -304,92 +303,6 @@ class StockItemReturnToStock(AjaxUpdateView):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockItemSelectLabels(AjaxView):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    View for selecting a template for printing labels for one (or more) StockItem objects
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    model = StockItem
 | 
					 | 
				
			||||||
    ajax_form_title = _('Select Label Template')
 | 
					 | 
				
			||||||
    role_required = 'stock.view'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_form(self):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        item = StockItem.objects.get(pk=self.kwargs['pk'])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        labels = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Construct a list of StockItemLabel objects which are enabled, and the filters match the selected StockItem
 | 
					 | 
				
			||||||
        for label in StockItemLabel.objects.filter(enabled=True):
 | 
					 | 
				
			||||||
            if label.matches_stock_item(item):
 | 
					 | 
				
			||||||
                labels.append(label)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return StockForms.StockItemLabelSelectForm(labels)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        label = request.POST.get('label', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            label = StockItemLabel.objects.get(pk=label)
 | 
					 | 
				
			||||||
        except (ValueError, StockItemLabel.DoesNotExist):
 | 
					 | 
				
			||||||
            raise ValidationError({'label': _("Select valid label")})
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
        stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        url = reverse('stock-item-print-labels')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        url += '?label={pk}'.format(pk=label.pk)
 | 
					 | 
				
			||||||
        url += '&items[]={pk}'.format(pk=stock_item.pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            'form_valid': True,
 | 
					 | 
				
			||||||
            'url': url,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.renderJsonResponse(request, self.get_form(), data=data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class StockItemPrintLabels(AjaxView):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    View for printing labels and returning a PDF
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Requires the following arguments to be passed as URL params:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    items: List of valid StockItem pk values
 | 
					 | 
				
			||||||
    label: Valid pk of a StockItemLabel template
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    role_required = 'stock.view'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        label = request.GET.get('label', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            label = StockItemLabel.objects.get(pk=label)
 | 
					 | 
				
			||||||
        except (ValueError, StockItemLabel.DoesNotExist):
 | 
					 | 
				
			||||||
            raise ValidationError({'label': 'Invalid label ID'})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        item_pks = request.GET.getlist('items[]')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        items = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for pk in item_pks:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                item = StockItem.objects.get(pk=pk)
 | 
					 | 
				
			||||||
                items.append(item)
 | 
					 | 
				
			||||||
            except (ValueError, StockItem.DoesNotExist):
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if len(items) == 0:
 | 
					 | 
				
			||||||
            raise ValidationError({'items': 'Must provide valid stockitems'})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        pdf = label.render(items).getbuffer()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return DownloadFile(pdf, 'stock_labels.pdf', content_type='application/pdf')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class StockItemDeleteTestData(AjaxUpdateView):
 | 
					class StockItemDeleteTestData(AjaxUpdateView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    View for deleting all test data
 | 
					    View for deleting all test data
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,7 +112,6 @@ InvenTree
 | 
				
			|||||||
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
 | 
					<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% static 'script/inventree/filters.js' %}"></script>
 | 
					<script type='text/javascript' src="{% static 'script/inventree/filters.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
 | 
					<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
 | 
					 | 
				
			||||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
 | 
					<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
 | 
					<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -120,6 +119,8 @@ InvenTree
 | 
				
			|||||||
<script type='text/javascript' src="{% url 'bom.js' %}"></script>
 | 
					<script type='text/javascript' src="{% url 'bom.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% url 'company.js' %}"></script>
 | 
					<script type='text/javascript' src="{% url 'company.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% url 'part.js' %}"></script>
 | 
					<script type='text/javascript' src="{% url 'part.js' %}"></script>
 | 
				
			||||||
 | 
					<script type='text/javascript' src="{% url 'modals.js' %}"></script>
 | 
				
			||||||
 | 
					<script type='text/javascript' src="{% url 'label.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% url 'stock.js' %}"></script>
 | 
					<script type='text/javascript' src="{% url 'stock.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% url 'build.js' %}"></script>
 | 
					<script type='text/javascript' src="{% url 'build.js' %}"></script>
 | 
				
			||||||
<script type='text/javascript' src="{% url 'order.js' %}"></script>
 | 
					<script type='text/javascript' src="{% url 'order.js' %}"></script>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										173
									
								
								InvenTree/templates/js/label.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								InvenTree/templates/js/label.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function printStockItemLabels(items, options={}) {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Print stock item labels for the given stock items
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (items.length == 0) {
 | 
				
			||||||
 | 
					        showAlertDialog(
 | 
				
			||||||
 | 
					            '{% trans "Select Stock Items" %}',
 | 
				
			||||||
 | 
					            '{% trans "Stock item(s) must be selected before printing labels" %}'
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Request available labels from the server
 | 
				
			||||||
 | 
					    inventreeGet(
 | 
				
			||||||
 | 
					        '{% url "api-stockitem-label-list" %}',
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            enabled: true,
 | 
				
			||||||
 | 
					            items: items,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            success: function(response) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (response.length == 0) {
 | 
				
			||||||
 | 
					                    showAlertDialog(
 | 
				
			||||||
 | 
					                        '{% trans "No Labels Found" %}',
 | 
				
			||||||
 | 
					                        '{% trans "No labels found which match selected stock item(s)" %}',
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Select label to print
 | 
				
			||||||
 | 
					                selectLabel(
 | 
				
			||||||
 | 
					                    response,
 | 
				
			||||||
 | 
					                    items,
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        success: function(pk) {
 | 
				
			||||||
 | 
					                            var href = `/api/label/stock/${pk}/print/?`;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					                            items.forEach(function(item) {
 | 
				
			||||||
 | 
					                                href += `items[]=${item}&`;
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                            window.location.href = href;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function printStockLocationLabels(locations, options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (locations.length == 0) {
 | 
				
			||||||
 | 
					        showAlertDialog(
 | 
				
			||||||
 | 
					            '{% trans "Select Stock Locations" %}',
 | 
				
			||||||
 | 
					            '{% trans "Stock location(s) must be selected before printing labels" %}'
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Request available labels from the server
 | 
				
			||||||
 | 
					    inventreeGet(
 | 
				
			||||||
 | 
					        '{% url "api-stocklocation-label-list" %}',
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            enabled: true,
 | 
				
			||||||
 | 
					            locations: locations,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            success: function(response) {
 | 
				
			||||||
 | 
					                if (response.length == 0) {
 | 
				
			||||||
 | 
					                    showAlertDialog(
 | 
				
			||||||
 | 
					                        '{% trans "No Labels Found" %}',
 | 
				
			||||||
 | 
					                        '{% trans "No labels found which match selected stock location(s)" %}',
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Select label to print
 | 
				
			||||||
 | 
					                selectLabel(
 | 
				
			||||||
 | 
					                    response,
 | 
				
			||||||
 | 
					                    locations,
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        success: function(pk) {
 | 
				
			||||||
 | 
					                            var href = `/api/label/location/${pk}/print/?`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            locations.forEach(function(location) {
 | 
				
			||||||
 | 
					                                href += `locations[]=${location}&`;
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            window.location.href = href;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function selectLabel(labels, items, options={}) {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Present the user with the available labels,
 | 
				
			||||||
 | 
					     * and allow them to select which label to print.
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * The intent is that the available labels have been requested
 | 
				
			||||||
 | 
					     * (via AJAX) from the server.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var modal = options.modal || '#modal-form';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var label_list = makeOptionsList(
 | 
				
			||||||
 | 
					        labels,
 | 
				
			||||||
 | 
					        function(item) {
 | 
				
			||||||
 | 
					            var text = item.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (item.description) {
 | 
				
			||||||
 | 
					                text += ` - ${item.description}`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return text;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        function(item) {
 | 
				
			||||||
 | 
					            return item.pk;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Construct form
 | 
				
			||||||
 | 
					    var html = `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
 | 
				
			||||||
 | 
					        <div class='form-group'>
 | 
				
			||||||
 | 
					            <label class='control-label requiredField' for='id_label'>
 | 
				
			||||||
 | 
					            {% trans "Select Label" %}
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					            <div class='controls'>
 | 
				
			||||||
 | 
					                <select id='id_label' class='select form-control name='label'>
 | 
				
			||||||
 | 
					                    ${label_list}
 | 
				
			||||||
 | 
					                </select>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </form>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    openModal({
 | 
				
			||||||
 | 
					        modal: modal,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    modalEnable(modal, true);
 | 
				
			||||||
 | 
					    modalSetTitle(modal, '{% trans "Select Label Template" %}');
 | 
				
			||||||
 | 
					    modalSetContent(modal, html);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attachSelect(modal);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    modalSubmit(modal, function() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var label = $(modal).find('#id_label');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var pk = label.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        closeModal(modal);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (options.success) {
 | 
				
			||||||
 | 
					            options.success(pk);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function makeOption(text, value, title) {
 | 
					function makeOption(text, value, title) {
 | 
				
			||||||
    /* Format an option for a select element
 | 
					    /* Format an option for a select element
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@@ -164,6 +166,15 @@ function setFieldValue(fieldName, value, options={}) {
 | 
				
			|||||||
    field.val(value);
 | 
					    field.val(value);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getFieldValue(fieldName, options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var modal = options.modal || '#modal-form';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var field = getFieldByName(modal, fieldName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return field.val();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function partialMatcher(params, data) {
 | 
					function partialMatcher(params, data) {
 | 
				
			||||||
    /* Replacement function for the 'matcher' parameter for a select2 dropdown.
 | 
					    /* Replacement function for the 'matcher' parameter for a select2 dropdown.
 | 
				
			||||||
@@ -392,7 +403,7 @@ function renderErrorMessage(xhr) {
 | 
				
			|||||||
        <div class='panel panel-default'>
 | 
					        <div class='panel panel-default'>
 | 
				
			||||||
            <div class='panel panel-heading'>
 | 
					            <div class='panel panel-heading'>
 | 
				
			||||||
                <div class='panel-title'>
 | 
					                <div class='panel-title'>
 | 
				
			||||||
                    <a data-toggle='collapse' href="#collapse-error-info">Show Error Information</a>
 | 
					                    <a data-toggle='collapse' href="#collapse-error-info">{% trans "Show Error Information" %}</a>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class='panel-collapse collapse' id='collapse-error-info'>
 | 
					            <div class='panel-collapse collapse' id='collapse-error-info'>
 | 
				
			||||||
@@ -459,8 +470,8 @@ function showQuestionDialog(title, content, options={}) {
 | 
				
			|||||||
    modalSetTitle(modal, title);
 | 
					    modalSetTitle(modal, title);
 | 
				
			||||||
    modalSetContent(modal, content);
 | 
					    modalSetContent(modal, content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var accept_text = options.accept_text || 'Accept';
 | 
					    var accept_text = options.accept_text || '{% trans "Accept" %}';
 | 
				
			||||||
    var cancel_text = options.cancel_text || 'Cancel';
 | 
					    var cancel_text = options.cancel_text || '{% trans "Cancel" %}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(modal).find('#modal-form-cancel').html(cancel_text);
 | 
					    $(modal).find('#modal-form-cancel').html(cancel_text);
 | 
				
			||||||
    $(modal).find('#modal-form-accept').html(accept_text);
 | 
					    $(modal).find('#modal-form-accept').html(accept_text);
 | 
				
			||||||
@@ -524,7 +535,7 @@ function openModal(options) {
 | 
				
			|||||||
    if (options.title) {
 | 
					    if (options.title) {
 | 
				
			||||||
        modalSetTitle(modal, options.title);
 | 
					        modalSetTitle(modal, options.title);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        modalSetTitle(modal, 'Loading Data...');
 | 
					        modalSetTitle(modal, '{% trans "Loading Data" %}...');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Unless the content is explicitly set, display loading message
 | 
					    // Unless the content is explicitly set, display loading message
 | 
				
			||||||
@@ -535,8 +546,8 @@ function openModal(options) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Default labels for 'Submit' and 'Close' buttons in the form
 | 
					    // Default labels for 'Submit' and 'Close' buttons in the form
 | 
				
			||||||
    var submit_text = options.submit_text || 'Submit';
 | 
					    var submit_text = options.submit_text || '{% trans "Submit" %}';
 | 
				
			||||||
    var close_text = options.close_text || 'Close';
 | 
					    var close_text = options.close_text || '{% trans "Close" %}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    modalSetButtonText(modal, submit_text, close_text);
 | 
					    modalSetButtonText(modal, submit_text, close_text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -745,7 +756,7 @@ function handleModalForm(url, options) {
 | 
				
			|||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        else {
 | 
					                        else {
 | 
				
			||||||
                            $(modal).modal('hide');
 | 
					                            $(modal).modal('hide');
 | 
				
			||||||
                            showAlertDialog('Invalid response from server', 'Form data missing from server response');
 | 
					                            showAlertDialog('{% trans "Invalid response from server" %}', '{% trans "Form data missing from server response" %}');
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -758,7 +769,7 @@ function handleModalForm(url, options) {
 | 
				
			|||||||
                // There was an error submitting form data via POST
 | 
					                // There was an error submitting form data via POST
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                $(modal).modal('hide'); 
 | 
					                $(modal).modal('hide'); 
 | 
				
			||||||
                showAlertDialog('Error posting form data', renderErrorMessage(xhr));                
 | 
					                showAlertDialog('{% trans "Error posting form data" %}', renderErrorMessage(xhr));                
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            complete: function(xhr) {
 | 
					            complete: function(xhr) {
 | 
				
			||||||
                //TODO
 | 
					                //TODO
 | 
				
			||||||
@@ -793,8 +804,8 @@ function launchModalForm(url, options = {}) {
 | 
				
			|||||||
    var modal = options.modal || '#modal-form';
 | 
					    var modal = options.modal || '#modal-form';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Default labels for 'Submit' and 'Close' buttons in the form
 | 
					    // Default labels for 'Submit' and 'Close' buttons in the form
 | 
				
			||||||
    var submit_text = options.submit_text || 'Submit';
 | 
					    var submit_text = options.submit_text || '{% trans "Submit" %}';
 | 
				
			||||||
    var close_text = options.close_text || 'Close';
 | 
					    var close_text = options.close_text || '{% trans "Close" %}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Form the ajax request to retrieve the django form data
 | 
					    // Form the ajax request to retrieve the django form data
 | 
				
			||||||
    ajax_data = {
 | 
					    ajax_data = {
 | 
				
			||||||
@@ -842,7 +853,7 @@ function launchModalForm(url, options = {}) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                $(modal).modal('hide');
 | 
					                $(modal).modal('hide');
 | 
				
			||||||
                showAlertDialog('Invalid server response', 'JSON response missing form data');
 | 
					                showAlertDialog('{% trans "Invalid server response" %}', '{% trans "JSON response missing form data" %}');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        error: function (xhr, ajaxOptions, thrownError) {
 | 
					        error: function (xhr, ajaxOptions, thrownError) {
 | 
				
			||||||
@@ -852,36 +863,36 @@ function launchModalForm(url, options = {}) {
 | 
				
			|||||||
            if (xhr.status == 0) {
 | 
					            if (xhr.status == 0) {
 | 
				
			||||||
                // No response from the server
 | 
					                // No response from the server
 | 
				
			||||||
                showAlertDialog(
 | 
					                showAlertDialog(
 | 
				
			||||||
                    "No Response",
 | 
					                    '{% trans "No Response" %}',
 | 
				
			||||||
                    "No response from the InvenTree server",
 | 
					                    '{% trans "No response from the InvenTree server" %}',
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            } else if (xhr.status == 400) {
 | 
					            } else if (xhr.status == 400) {
 | 
				
			||||||
                showAlertDialog(
 | 
					                showAlertDialog(
 | 
				
			||||||
                    "Error 400: Bad Request",
 | 
					                    '{% trans "Error 400: Bad Request" %}',
 | 
				
			||||||
                    "Server returned error code 400"
 | 
					                    '{% trans "Server returned error code 400" %}',
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            } else if (xhr.status == 401) {
 | 
					            } else if (xhr.status == 401) {
 | 
				
			||||||
                showAlertDialog(
 | 
					                showAlertDialog(
 | 
				
			||||||
                    "Error 401: Not Authenticated",
 | 
					                    '{% trans "Error 401: Not Authenticated" %}',
 | 
				
			||||||
                    "Authentication credentials not supplied"
 | 
					                    '{% trans "Authentication credentials not supplied" %}',
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            } else if (xhr.status == 403) {
 | 
					            } else if (xhr.status == 403) {
 | 
				
			||||||
                showAlertDialog(
 | 
					                showAlertDialog(
 | 
				
			||||||
                    "Error 403: Permission Denied",
 | 
					                    '{% trans "Error 403: Permission Denied" %}',
 | 
				
			||||||
                    "You do not have the required permissions to access this function"
 | 
					                    '{% trans "You do not have the required permissions to access this function" %}',
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            } else if (xhr.status == 404) {
 | 
					            } else if (xhr.status == 404) {
 | 
				
			||||||
                showAlertDialog(
 | 
					                showAlertDialog(
 | 
				
			||||||
                    "Error 404: Resource Not Found",
 | 
					                    '{% trans "Error 404: Resource Not Found" %}',
 | 
				
			||||||
                    "The requested resource could not be located on the server"
 | 
					                    '{% trans "The requested resource could not be located on the server" %}',
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            } else if (xhr.status == 408) {
 | 
					            } else if (xhr.status == 408) {
 | 
				
			||||||
                showAlertDialog(
 | 
					                showAlertDialog(
 | 
				
			||||||
                    "Error 408: Timeout",
 | 
					                    '{% trans "Error 408: Timeout" %}',
 | 
				
			||||||
                    "Connection timeout while requesting data from server"
 | 
					                    '{% trans "Connection timeout while requesting data from server" %}',
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                showAlertDialog('Error requesting form data', renderErrorMessage(xhr));
 | 
					                showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log("Modal form error: " + xhr.status);
 | 
					            console.log("Modal form error: " + xhr.status);
 | 
				
			||||||
@@ -660,6 +660,18 @@ function loadStockTable(table, options) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Automatically link button callbacks
 | 
					    // Automatically link button callbacks
 | 
				
			||||||
 | 
					    $('#multi-item-print-label').click(function() {
 | 
				
			||||||
 | 
					        var selections = $('#stock-table').bootstrapTable('getSelections');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var items = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        selections.forEach(function(item) {
 | 
				
			||||||
 | 
					            items.push(item.pk);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        printStockItemLabels(items);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('#multi-item-stocktake').click(function() {
 | 
					    $('#multi-item-stocktake').click(function() {
 | 
				
			||||||
        stockAdjustment('count');
 | 
					        stockAdjustment('count');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@
 | 
				
			|||||||
            <div class="btn-group">
 | 
					            <div class="btn-group">
 | 
				
			||||||
                <button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
 | 
					                <button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
 | 
				
			||||||
                <ul class="dropdown-menu">
 | 
					                <ul class="dropdown-menu">
 | 
				
			||||||
 | 
					                    <li><a href='#' id='multi-item-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
 | 
				
			||||||
                    {% if roles.stock.change %}
 | 
					                    {% if roles.stock.change %}
 | 
				
			||||||
                    <li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
 | 
					                    <li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
 | 
				
			||||||
                    <li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
 | 
					                    <li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,6 +112,7 @@ class RuleSet(models.Model):
 | 
				
			|||||||
        'common_inventreesetting',
 | 
					        'common_inventreesetting',
 | 
				
			||||||
        'company_contact',
 | 
					        'company_contact',
 | 
				
			||||||
        'label_stockitemlabel',
 | 
					        'label_stockitemlabel',
 | 
				
			||||||
 | 
					        'label_stocklocationlabel',
 | 
				
			||||||
        'report_reportasset',
 | 
					        'report_reportasset',
 | 
				
			||||||
        'report_testreport',
 | 
					        'report_testreport',
 | 
				
			||||||
        'part_partstar',
 | 
					        'part_partstar',
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user