mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
522432f4aa
@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import StockItemLabel, StockLocationLabel
|
||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||
|
||||
|
||||
class LabelAdmin(admin.ModelAdmin):
|
||||
@ -13,3 +13,4 @@ class LabelAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(StockItemLabel, LabelAdmin)
|
||||
admin.site.register(StockLocationLabel, LabelAdmin)
|
||||
admin.site.register(PartLabel, LabelAdmin)
|
||||
|
@ -15,9 +15,10 @@ import InvenTree.helpers
|
||||
import common.models
|
||||
|
||||
from stock.models import StockItem, StockLocation
|
||||
from part.models import Part
|
||||
|
||||
from .models import StockItemLabel, StockLocationLabel
|
||||
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer
|
||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
|
||||
|
||||
|
||||
class LabelListView(generics.ListAPIView):
|
||||
@ -132,6 +133,7 @@ class StockItemLabelMixin:
|
||||
for key in ['item', 'item[]', 'items', 'items[]']:
|
||||
if key in params:
|
||||
items = params.getlist(key, [])
|
||||
break
|
||||
|
||||
valid_ids = []
|
||||
|
||||
@ -376,6 +378,112 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin,
|
||||
return self.print(request, locations)
|
||||
|
||||
|
||||
class PartLabelMixin:
|
||||
"""
|
||||
Mixin for extracting Part objects from query parameters
|
||||
"""
|
||||
|
||||
def get_parts(self):
|
||||
"""
|
||||
Return a list of requested Part objects
|
||||
"""
|
||||
|
||||
parts = []
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
for key in ['part', 'part[]', 'parts', 'parts[]']:
|
||||
if key in params:
|
||||
parts = params.getlist(key, [])
|
||||
break
|
||||
|
||||
valid_ids = []
|
||||
|
||||
for part in parts:
|
||||
try:
|
||||
valid_ids.append(int(part))
|
||||
except (ValueError):
|
||||
pass
|
||||
|
||||
# List of Part objects which match provided values
|
||||
return Part.objects.filter(pk__in=valid_ids)
|
||||
|
||||
|
||||
class PartLabelList(LabelListView, PartLabelMixin):
|
||||
"""
|
||||
API endpoint for viewing list of PartLabel objects
|
||||
"""
|
||||
|
||||
queryset = PartLabel.objects.all()
|
||||
serializer_class = PartLabelSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
parts = self.get_parts()
|
||||
|
||||
if len(parts) > 0:
|
||||
|
||||
valid_label_ids = set()
|
||||
|
||||
for label in queryset.all():
|
||||
|
||||
matches = True
|
||||
|
||||
try:
|
||||
filters = InvenTree.helpers.validateFilterString(label.filters)
|
||||
except ValidationError:
|
||||
continue
|
||||
|
||||
for part in parts:
|
||||
|
||||
part_query = Part.objects.filter(pk=part.pk)
|
||||
|
||||
try:
|
||||
if not part_query.filter(**filters).exists():
|
||||
matches = False
|
||||
break
|
||||
except FieldError:
|
||||
matches = False
|
||||
break
|
||||
|
||||
if matches:
|
||||
valid_label_ids.add(label.pk)
|
||||
|
||||
# Reduce queryset to only valid matches
|
||||
queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids])
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class PartLabelDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for a single PartLabel object
|
||||
"""
|
||||
|
||||
queryset = PartLabel.objects.all()
|
||||
serializer_class = PartLabelSerializer
|
||||
|
||||
|
||||
class PartLabelPrint(generics.RetrieveAPIView, PartLabelMixin, LabelPrintMixin):
|
||||
"""
|
||||
API endpoint for printing a PartLabel object
|
||||
"""
|
||||
|
||||
queryset = PartLabel.objects.all()
|
||||
serializer_class = PartLabelSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Check if valid part(s) have been provided
|
||||
"""
|
||||
|
||||
parts = self.get_parts()
|
||||
|
||||
return self.print(request, parts)
|
||||
|
||||
|
||||
label_api_urls = [
|
||||
|
||||
# Stock item labels
|
||||
@ -401,4 +509,16 @@ label_api_urls = [
|
||||
# List view
|
||||
url(r'^.*$', StockLocationLabelList.as_view(), name='api-stocklocation-label-list'),
|
||||
])),
|
||||
|
||||
# Part labels
|
||||
url(r'^part/', include([
|
||||
# Detail views
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^print/', PartLabelPrint.as_view(), name='api-part-label-print'),
|
||||
url(r'^.*$', PartLabelDetail.as_view(), name='api-part-label-detail'),
|
||||
])),
|
||||
|
||||
# List view
|
||||
url(r'^.*$', PartLabelList.as_view(), name='api-part-label-list'),
|
||||
])),
|
||||
]
|
||||
|
@ -37,6 +37,7 @@ class LabelConfig(AppConfig):
|
||||
if canAppAccessDatabase():
|
||||
self.create_stock_item_labels()
|
||||
self.create_stock_location_labels()
|
||||
self.create_part_labels()
|
||||
|
||||
def create_stock_item_labels(self):
|
||||
"""
|
||||
@ -65,7 +66,7 @@ class LabelConfig(AppConfig):
|
||||
)
|
||||
|
||||
if not os.path.exists(dst_dir):
|
||||
logger.info(f"Creating missing directory: '{dst_dir}'")
|
||||
logger.info(f"Creating required directory: '{dst_dir}'")
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
|
||||
labels = [
|
||||
@ -109,24 +110,21 @@ class LabelConfig(AppConfig):
|
||||
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
|
||||
# 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']}'")
|
||||
logger.info(f"Creating entry for StockItemLabel '{label['name']}'")
|
||||
|
||||
StockItemLabel.objects.create(
|
||||
name=label['name'],
|
||||
description=label['description'],
|
||||
label=filename,
|
||||
filters='',
|
||||
enabled=True,
|
||||
width=label['width'],
|
||||
height=label['height'],
|
||||
)
|
||||
except:
|
||||
pass
|
||||
StockItemLabel.objects.create(
|
||||
name=label['name'],
|
||||
description=label['description'],
|
||||
label=filename,
|
||||
filters='',
|
||||
enabled=True,
|
||||
width=label['width'],
|
||||
height=label['height'],
|
||||
)
|
||||
|
||||
def create_stock_location_labels(self):
|
||||
"""
|
||||
@ -155,7 +153,7 @@ class LabelConfig(AppConfig):
|
||||
)
|
||||
|
||||
if not os.path.exists(dst_dir):
|
||||
logger.info(f"Creating missing directory: '{dst_dir}'")
|
||||
logger.info(f"Creating required directory: '{dst_dir}'")
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
|
||||
labels = [
|
||||
@ -206,21 +204,103 @@ class LabelConfig(AppConfig):
|
||||
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
|
||||
# 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']}'")
|
||||
logger.info(f"Creating entry for StockLocationLabel '{label['name']}'")
|
||||
|
||||
StockLocationLabel.objects.create(
|
||||
name=label['name'],
|
||||
description=label['description'],
|
||||
label=filename,
|
||||
filters='',
|
||||
enabled=True,
|
||||
width=label['width'],
|
||||
height=label['height'],
|
||||
)
|
||||
except:
|
||||
pass
|
||||
StockLocationLabel.objects.create(
|
||||
name=label['name'],
|
||||
description=label['description'],
|
||||
label=filename,
|
||||
filters='',
|
||||
enabled=True,
|
||||
width=label['width'],
|
||||
height=label['height'],
|
||||
)
|
||||
|
||||
def create_part_labels(self):
|
||||
"""
|
||||
Create database entries for the default PartLabel templates,
|
||||
if they do not already exist.
|
||||
"""
|
||||
|
||||
try:
|
||||
from .models import PartLabel
|
||||
except:
|
||||
# Database might not yet be ready
|
||||
return
|
||||
|
||||
src_dir = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'templates',
|
||||
'label',
|
||||
'part',
|
||||
)
|
||||
|
||||
dst_dir = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
'label',
|
||||
'inventree',
|
||||
'part',
|
||||
)
|
||||
|
||||
if not os.path.exists(dst_dir):
|
||||
logger.info(f"Creating required directory: '{dst_dir}'")
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
|
||||
labels = [
|
||||
{
|
||||
'file': 'part_label.html',
|
||||
'name': 'Part Label',
|
||||
'description': 'Simple part label',
|
||||
'width': 70,
|
||||
'height': 24,
|
||||
},
|
||||
]
|
||||
|
||||
for label in labels:
|
||||
|
||||
filename = os.path.join(
|
||||
'label',
|
||||
'inventree',
|
||||
'part',
|
||||
label['file']
|
||||
)
|
||||
|
||||
src_file = os.path.join(src_dir, label['file'])
|
||||
dst_file = os.path.join(settings.MEDIA_ROOT, filename)
|
||||
|
||||
to_copy = False
|
||||
|
||||
if os.path.exists(dst_file):
|
||||
# File already exists - let's see if it is the "same"
|
||||
|
||||
if not hashFile(dst_file) == hashFile(src_file):
|
||||
logger.info(f"Hash differs for '{filename}'")
|
||||
to_copy = True
|
||||
|
||||
else:
|
||||
logger.info(f"Label template '{filename}' is not present")
|
||||
to_copy = True
|
||||
|
||||
if to_copy:
|
||||
logger.info(f"Copying label template '{dst_file}'")
|
||||
shutil.copyfile(src_file, dst_file)
|
||||
|
||||
# Check if a label matching the template already exists
|
||||
if PartLabel.objects.filter(label=filename).exists():
|
||||
continue
|
||||
|
||||
logger.info(f"Creating entry for PartLabel '{label['name']}'")
|
||||
|
||||
PartLabel.objects.create(
|
||||
name=label['name'],
|
||||
description=label['description'],
|
||||
label=filename,
|
||||
filters='',
|
||||
enabled=True,
|
||||
width=label['width'],
|
||||
height=label['height'],
|
||||
)
|
||||
|
37
InvenTree/label/migrations/0008_auto_20210708_2106.py
Normal file
37
InvenTree/label/migrations/0008_auto_20210708_2106.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-08 11:06
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import label.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('label', '0007_auto_20210513_1327'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PartLabel',
|
||||
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, verbose_name='Name')),
|
||||
('description', models.CharField(blank=True, help_text='Label description', max_length=250, null=True, verbose_name='Description')),
|
||||
('label', 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')),
|
||||
('enabled', models.BooleanField(default=True, help_text='Label template is enabled', verbose_name='Enabled')),
|
||||
('width', models.FloatField(default=50, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]')),
|
||||
('height', models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]')),
|
||||
('filename_pattern', models.CharField(default='label.pdf', help_text='Pattern for generating label filenames', max_length=100, verbose_name='Filename Pattern')),
|
||||
('filters', models.CharField(blank=True, help_text='Part query filters (comma-separated value of key=value pairs)', max_length=250, validators=[label.models.validate_part_filters], verbose_name='Filters')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
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'),
|
||||
),
|
||||
]
|
@ -25,6 +25,8 @@ from InvenTree.helpers import validateFilterString, normalize
|
||||
|
||||
import common.models
|
||||
import stock.models
|
||||
import part.models
|
||||
|
||||
|
||||
try:
|
||||
from django_weasyprint import WeasyTemplateResponseMixin
|
||||
@ -59,6 +61,13 @@ def validate_stock_location_filters(filters):
|
||||
return filters
|
||||
|
||||
|
||||
def validate_part_filters(filters):
|
||||
|
||||
filters = validateFilterString(filters, model=part.models.Part)
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
class WeasyprintLabelMixin(WeasyTemplateResponseMixin):
|
||||
"""
|
||||
Class for rendering a label to a PDF
|
||||
@ -246,10 +255,11 @@ class StockItemLabel(LabelTemplate):
|
||||
|
||||
filters = models.CharField(
|
||||
blank=True, max_length=250,
|
||||
help_text=_('Query filters (comma-separated list of key=value pairs'),
|
||||
help_text=_('Query filters (comma-separated list of key=value pairs),'),
|
||||
verbose_name=_('Filters'),
|
||||
validators=[
|
||||
validate_stock_item_filters]
|
||||
validate_stock_item_filters
|
||||
]
|
||||
)
|
||||
|
||||
def matches_stock_item(self, item):
|
||||
@ -335,3 +345,57 @@ class StockLocationLabel(LabelTemplate):
|
||||
'location': location,
|
||||
'qr_data': location.format_barcode(brief=True),
|
||||
}
|
||||
|
||||
|
||||
class PartLabel(LabelTemplate):
|
||||
"""
|
||||
Template for printing Part labels
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-label-list')
|
||||
|
||||
SUBDIR = 'part'
|
||||
|
||||
filters = models.CharField(
|
||||
blank=True, max_length=250,
|
||||
help_text=_('Part query filters (comma-separated value of key=value pairs)'),
|
||||
verbose_name=_('Filters'),
|
||||
validators=[
|
||||
validate_part_filters
|
||||
]
|
||||
)
|
||||
|
||||
def matches_part(self, part):
|
||||
"""
|
||||
Test if this label template matches a given Part object
|
||||
"""
|
||||
|
||||
try:
|
||||
filters = validateFilterString(self.filters)
|
||||
parts = part.models.Part.objects.filter(**filters)
|
||||
except (ValidationError, FieldError):
|
||||
return False
|
||||
|
||||
parts = parts.filter(pk=part.pk)
|
||||
|
||||
return parts.exists()
|
||||
|
||||
def get_context_data(self, request):
|
||||
"""
|
||||
Generate context data for each provided Part object
|
||||
"""
|
||||
|
||||
part = self.object_to_print
|
||||
|
||||
return {
|
||||
'part': part,
|
||||
'category': part.category,
|
||||
'name': part.name,
|
||||
'description': part.description,
|
||||
'IPN': part.IPN,
|
||||
'revision': part.revision,
|
||||
'qr_data': part.format_barcode(brief=True),
|
||||
'qr_url': part.format_barcode(url=True, request=request),
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||
from InvenTree.serializers import InvenTreeModelSerializer
|
||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||
|
||||
from .models import StockItemLabel, StockLocationLabel
|
||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||
|
||||
|
||||
class StockItemLabelSerializer(InvenTreeModelSerializer):
|
||||
@ -43,3 +43,22 @@ class StockLocationLabelSerializer(InvenTreeModelSerializer):
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
|
||||
class PartLabelSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializes a PartLabel object
|
||||
"""
|
||||
|
||||
label = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = PartLabel
|
||||
fields = [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'label',
|
||||
'filters',
|
||||
'enabled',
|
||||
]
|
||||
|
33
InvenTree/label/templates/label/part/part_label.html
Normal file
33
InvenTree/label/templates/label/part/part_label.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends "label/label_base.html" %}
|
||||
|
||||
{% load barcode %}
|
||||
|
||||
{% block style %}
|
||||
|
||||
.qr {
|
||||
position: fixed;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
height: {{ height }}mm;
|
||||
width: {{ height }}mm;
|
||||
}
|
||||
|
||||
.part {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
display: inline;
|
||||
position: absolute;
|
||||
left: {{ height }}mm;
|
||||
top: 2mm;
|
||||
}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<img class='qr' src='{% qrcode qr_data %}'>
|
||||
|
||||
<div class='part'>
|
||||
{{ part.full_name }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -268,6 +268,10 @@
|
||||
);
|
||||
});
|
||||
|
||||
$('#print-label').click(function() {
|
||||
printPartLabels([{{ part.pk }}]);
|
||||
});
|
||||
|
||||
$("#part-count").click(function() {
|
||||
launchModalForm("/stock/adjust/", {
|
||||
data: {
|
||||
|
@ -105,6 +105,61 @@ function printStockLocationLabels(locations, options={}) {
|
||||
}
|
||||
|
||||
|
||||
function printPartLabels(parts, options={}) {
|
||||
/**
|
||||
* Print labels for the provided parts
|
||||
*/
|
||||
|
||||
if (parts.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Parts" %}',
|
||||
'{% trans "Part(s) must be selected before printing labels" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Request available labels from the server
|
||||
inventreeGet(
|
||||
'{% url "api-part-label-list" %}',
|
||||
{
|
||||
enabled: true,
|
||||
parts: parts,
|
||||
},
|
||||
{
|
||||
success: function(response) {
|
||||
|
||||
if (response.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "No Labels Found" %}',
|
||||
'{% trans "No labels found which match the selected part(s)" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select label to print
|
||||
selectLabel(
|
||||
response,
|
||||
parts,
|
||||
{
|
||||
success: function(pk) {
|
||||
var url = `/api/label/part/${pk}/print/?`;
|
||||
|
||||
parts.forEach(function(part) {
|
||||
url += `parts[]=${part}&`;
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function selectLabel(labels, items, options={}) {
|
||||
/**
|
||||
* Present the user with the available labels,
|
||||
|
@ -87,6 +87,7 @@ class RuleSet(models.Model):
|
||||
'company_supplierpart',
|
||||
'company_manufacturerpart',
|
||||
'company_manufacturerpartparameter',
|
||||
'label_partlabel',
|
||||
],
|
||||
'stock_location': [
|
||||
'stock_stocklocation',
|
||||
|
Loading…
Reference in New Issue
Block a user