mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
aada0ca5af
@ -12,7 +12,7 @@ from decimal import Decimal
|
||||
|
||||
from wsgiref.util import FileWrapper
|
||||
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.contrib.auth.models import Permission
|
||||
@ -414,7 +414,7 @@ def extract_serial_numbers(serials, expected_quantity):
|
||||
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
|
||||
|
||||
@ -464,6 +464,15 @@ def validateFilterString(value):
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -28,6 +28,7 @@ from company.api import company_api_urls
|
||||
from stock.api import stock_api_urls
|
||||
from build.api import build_api_urls
|
||||
from order.api import order_api_urls
|
||||
from label.api import label_api_urls
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
@ -58,6 +59,7 @@ apipatterns = [
|
||||
url(r'^stock/', include(stock_api_urls)),
|
||||
url(r'^build/', include(build_api_urls)),
|
||||
url(r'^order/', include(order_api_urls)),
|
||||
url(r'^label/', include(label_api_urls)),
|
||||
|
||||
# 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
|
||||
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'^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'),
|
||||
@ -97,6 +100,7 @@ dynamic_javascript_urls = [
|
||||
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'^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'^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 .models import StockItemLabel
|
||||
from .models import StockItemLabel, StockLocationLabel
|
||||
|
||||
|
||||
class StockItemLabelAdmin(admin.ModelAdmin):
|
||||
class LabelAdmin(admin.ModelAdmin):
|
||||
|
||||
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.conf import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LabelConfig(AppConfig):
|
||||
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 stock.models import StockItem
|
||||
import stock.models
|
||||
|
||||
|
||||
def rename_label(instance, filename):
|
||||
@ -28,6 +28,20 @@ def rename_label(instance, 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):
|
||||
"""
|
||||
Base class for generic, filterable labels.
|
||||
@ -50,30 +64,31 @@ class LabelTemplate(models.Model):
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
unique=True,
|
||||
blank=False, max_length=100,
|
||||
verbose_name=_('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(
|
||||
upload_to=rename_label,
|
||||
unique=True,
|
||||
blank=False, null=False,
|
||||
verbose_name=_('Label'),
|
||||
help_text=_('Label template file'),
|
||||
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(
|
||||
default=True,
|
||||
verbose_name=_('Enabled'),
|
||||
help_text=_('Label template is enabled'),
|
||||
verbose_name=_('Enabled')
|
||||
)
|
||||
|
||||
def get_record_data(self, items):
|
||||
@ -117,6 +132,14 @@ class StockItemLabel(LabelTemplate):
|
||||
|
||||
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):
|
||||
"""
|
||||
Test if this label template matches a given StockItem object
|
||||
@ -124,7 +147,7 @@ class StockItemLabel(LabelTemplate):
|
||||
|
||||
filters = validateFilterString(self.filters)
|
||||
|
||||
items = StockItem.objects.filter(**filters)
|
||||
items = stock.models.StockItem.objects.filter(**filters)
|
||||
|
||||
items = items.filter(pk=item.pk)
|
||||
|
||||
@ -153,3 +176,47 @@ class StockItemLabel(LabelTemplate):
|
||||
})
|
||||
|
||||
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 report.models
|
||||
import label.models
|
||||
|
||||
from InvenTree.status_codes import StockStatus
|
||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
||||
@ -63,6 +64,13 @@ class StockLocation(InvenTreeTree):
|
||||
**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):
|
||||
""" Return a queryset for all stock items under this category.
|
||||
|
||||
@ -330,6 +338,13 @@ class StockItem(MPTTModel):
|
||||
**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"))
|
||||
|
||||
parent = TreeForeignKey(
|
||||
@ -1333,14 +1348,31 @@ class StockItem(MPTTModel):
|
||||
|
||||
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
|
||||
def has_labels(self):
|
||||
"""
|
||||
Return True if there are any label templates available for this stock item
|
||||
"""
|
||||
|
||||
# TODO - Implement this
|
||||
return True
|
||||
return len(self.available_labels()) > 0
|
||||
|
||||
|
||||
@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() {
|
||||
launchModalForm(
|
||||
"{% url 'stock-item-label-select' item.id %}",
|
||||
{
|
||||
follow: true,
|
||||
}
|
||||
)
|
||||
printStockItemLabels([{{ item.pk }}]);
|
||||
});
|
||||
|
||||
$("#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>
|
||||
<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 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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -205,6 +205,15 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#print-label').click(function() {
|
||||
|
||||
var locs = [{{ location.pk }}];
|
||||
|
||||
printStockLocationLabels(locs);
|
||||
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
$('#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'^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'^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/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
|
||||
url(r'^item/attachment/', include([
|
||||
|
@ -31,7 +31,6 @@ from datetime import datetime, timedelta
|
||||
from company.models import Company, SupplierPart
|
||||
from part.models import Part
|
||||
from report.models import TestReport
|
||||
from label.models import StockItemLabel
|
||||
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
|
||||
|
||||
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):
|
||||
"""
|
||||
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/filters.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/sidenav.js' %}"></script>
|
||||
|
||||
@ -120,6 +119,8 @@ InvenTree
|
||||
<script type='text/javascript' src="{% url 'bom.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 '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 'build.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) {
|
||||
/* Format an option for a select element
|
||||
*/
|
||||
@ -164,6 +166,15 @@ function setFieldValue(fieldName, value, options={}) {
|
||||
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) {
|
||||
/* 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-heading'>
|
||||
<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 class='panel-collapse collapse' id='collapse-error-info'>
|
||||
@ -459,8 +470,8 @@ function showQuestionDialog(title, content, options={}) {
|
||||
modalSetTitle(modal, title);
|
||||
modalSetContent(modal, content);
|
||||
|
||||
var accept_text = options.accept_text || 'Accept';
|
||||
var cancel_text = options.cancel_text || 'Cancel';
|
||||
var accept_text = options.accept_text || '{% trans "Accept" %}';
|
||||
var cancel_text = options.cancel_text || '{% trans "Cancel" %}';
|
||||
|
||||
$(modal).find('#modal-form-cancel').html(cancel_text);
|
||||
$(modal).find('#modal-form-accept').html(accept_text);
|
||||
@ -524,7 +535,7 @@ function openModal(options) {
|
||||
if (options.title) {
|
||||
modalSetTitle(modal, options.title);
|
||||
} else {
|
||||
modalSetTitle(modal, 'Loading Data...');
|
||||
modalSetTitle(modal, '{% trans "Loading Data" %}...');
|
||||
}
|
||||
|
||||
// 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
|
||||
var submit_text = options.submit_text || 'Submit';
|
||||
var close_text = options.close_text || 'Close';
|
||||
var submit_text = options.submit_text || '{% trans "Submit" %}';
|
||||
var close_text = options.close_text || '{% trans "Close" %}';
|
||||
|
||||
modalSetButtonText(modal, submit_text, close_text);
|
||||
|
||||
@ -745,7 +756,7 @@ function handleModalForm(url, options) {
|
||||
}
|
||||
else {
|
||||
$(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
|
||||
|
||||
$(modal).modal('hide');
|
||||
showAlertDialog('Error posting form data', renderErrorMessage(xhr));
|
||||
showAlertDialog('{% trans "Error posting form data" %}', renderErrorMessage(xhr));
|
||||
},
|
||||
complete: function(xhr) {
|
||||
//TODO
|
||||
@ -793,8 +804,8 @@ function launchModalForm(url, options = {}) {
|
||||
var modal = options.modal || '#modal-form';
|
||||
|
||||
// Default labels for 'Submit' and 'Close' buttons in the form
|
||||
var submit_text = options.submit_text || 'Submit';
|
||||
var close_text = options.close_text || 'Close';
|
||||
var submit_text = options.submit_text || '{% trans "Submit" %}';
|
||||
var close_text = options.close_text || '{% trans "Close" %}';
|
||||
|
||||
// Form the ajax request to retrieve the django form data
|
||||
ajax_data = {
|
||||
@ -842,7 +853,7 @@ function launchModalForm(url, options = {}) {
|
||||
|
||||
} else {
|
||||
$(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) {
|
||||
@ -852,36 +863,36 @@ function launchModalForm(url, options = {}) {
|
||||
if (xhr.status == 0) {
|
||||
// No response from the server
|
||||
showAlertDialog(
|
||||
"No Response",
|
||||
"No response from the InvenTree server",
|
||||
'{% trans "No Response" %}',
|
||||
'{% trans "No response from the InvenTree server" %}',
|
||||
);
|
||||
} else if (xhr.status == 400) {
|
||||
showAlertDialog(
|
||||
"Error 400: Bad Request",
|
||||
"Server returned error code 400"
|
||||
'{% trans "Error 400: Bad Request" %}',
|
||||
'{% trans "Server returned error code 400" %}',
|
||||
);
|
||||
} else if (xhr.status == 401) {
|
||||
showAlertDialog(
|
||||
"Error 401: Not Authenticated",
|
||||
"Authentication credentials not supplied"
|
||||
'{% trans "Error 401: Not Authenticated" %}',
|
||||
'{% trans "Authentication credentials not supplied" %}',
|
||||
);
|
||||
} else if (xhr.status == 403) {
|
||||
showAlertDialog(
|
||||
"Error 403: Permission Denied",
|
||||
"You do not have the required permissions to access this function"
|
||||
'{% trans "Error 403: Permission Denied" %}',
|
||||
'{% trans "You do not have the required permissions to access this function" %}',
|
||||
);
|
||||
} else if (xhr.status == 404) {
|
||||
showAlertDialog(
|
||||
"Error 404: Resource Not Found",
|
||||
"The requested resource could not be located on the server"
|
||||
'{% trans "Error 404: Resource Not Found" %}',
|
||||
'{% trans "The requested resource could not be located on the server" %}',
|
||||
);
|
||||
} else if (xhr.status == 408) {
|
||||
showAlertDialog(
|
||||
"Error 408: Timeout",
|
||||
"Connection timeout while requesting data from server"
|
||||
'{% trans "Error 408: Timeout" %}',
|
||||
'{% trans "Connection timeout while requesting data from server" %}',
|
||||
);
|
||||
} else {
|
||||
showAlertDialog('Error requesting form data', renderErrorMessage(xhr));
|
||||
showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr));
|
||||
}
|
||||
|
||||
console.log("Modal form error: " + xhr.status);
|
@ -660,6 +660,18 @@ function loadStockTable(table, options) {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
stockAdjustment('count');
|
||||
});
|
||||
|
@ -17,6 +17,7 @@
|
||||
<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>
|
||||
<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 %}
|
||||
<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>
|
||||
|
@ -112,6 +112,7 @@ class RuleSet(models.Model):
|
||||
'common_inventreesetting',
|
||||
'company_contact',
|
||||
'label_stockitemlabel',
|
||||
'label_stocklocationlabel',
|
||||
'report_reportasset',
|
||||
'report_testreport',
|
||||
'part_partstar',
|
||||
|
Loading…
Reference in New Issue
Block a user