diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 9880662e63..dbd59eecb0 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -383,3 +383,56 @@ def ExtractSerialNumbers(serials, expected_quantity): raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})".format(s=len(numbers), q=expected_quantity))]) return numbers + + +def validateFilterString(value): + """ + Validate that a provided filter string looks like a list of comma-separated key=value pairs + + These should nominally match to a valid database filter based on the model being filtered. + + e.g. "category=6, IPN=12" + e.g. "part__name=widget" + + The ReportTemplate class uses the filter string to work out which items a given report applies to. + For example, an acceptance test report template might only apply to stock items with a given IPN, + so the string could be set to: + + filters = "IPN = ACME0001" + + Returns a map of key:value pairs + """ + + # Empty results map + results = {} + + value = str(value).strip() + + if not value or len(value) == 0: + return results + + groups = value.split(',') + + for group in groups: + group = group.strip() + + pair = group.split('=') + + if not len(pair) == 2: + raise ValidationError( + "Invalid group: {g}".format(g=group) + ) + + k, v = pair + + k = k.strip() + v = v.strip() + + if not k or not v: + raise ValidationError( + "Invalid group: {g}".format(g=group) + ) + + results[k] = v + + return results \ No newline at end of file diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index fb68f65497..d3a5ee919d 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -130,6 +130,7 @@ INSTALLED_APPS = [ 'build.apps.BuildConfig', 'common.apps.CommonConfig', 'company.apps.CompanyConfig', + 'label.apps.LabelConfig', 'order.apps.OrderConfig', 'part.apps.PartConfig', 'report.apps.ReportConfig', diff --git a/InvenTree/label/__init__.py b/InvenTree/label/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/label/admin.py b/InvenTree/label/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/InvenTree/label/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py new file mode 100644 index 0000000000..ea4fa152ff --- /dev/null +++ b/InvenTree/label/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LabelConfig(AppConfig): + name = 'label' diff --git a/InvenTree/label/migrations/0001_initial.py b/InvenTree/label/migrations/0001_initial.py new file mode 100644 index 0000000000..e960bcef67 --- /dev/null +++ b/InvenTree/label/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.7 on 2020-08-15 23:27 + +import InvenTree.helpers +import django.core.validators +from django.db import migrations, models +import label.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='StockItemLabel', + 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])), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/InvenTree/label/migrations/__init__.py b/InvenTree/label/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py new file mode 100644 index 0000000000..74c811a633 --- /dev/null +++ b/InvenTree/label/models.py @@ -0,0 +1,93 @@ +""" +Label printing models +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + +from blabel import LabelWriter + +from django.db import models +from django.core.validators import FileExtensionValidator + +from django.utils.translation import gettext_lazy as _ + +from InvenTree.helpers import validateFilterString + +from stock.models import StockItem + + +def rename_label(instance, filename): + """ Place the label file into the correct subdirectory """ + + filename = os.path.basename(filename) + + return os.path.join('label', 'template', instance.SUBDIR, filename) + + +class LabelTemplate(models.Model): + """ + Base class for generic, filterable labels. + """ + + class Meta: + abstract = True + + # Each class of label files will be stored in a separate subdirectory + SUBDIR = "label" + + name = models.CharField( + unique=True, + blank=False, max_length=100, + help_text=_('Label name'), + ) + + description = models.CharField(max_length=250, help_text=_('Label description'), blank=True, null=True) + + label = models.FileField( + upload_to=rename_label, + blank=False, null=False, + 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] + ) + + def get_record_data(self, items): + + return [] + + def render(self, items, **kwargs): + + records = self.get_record_data(items) + + writer = LabelWriter(self.label.filename) + + writer.write_label(records, 'out.pdf') + + +class StockItemLabel(LabelTemplate): + """ + Template for printing StockItem labels + """ + + SUBDIR = "stockitem" + + def matches_stock_item(self, item): + """ + Test if this label template matches a given StockItem object + """ + + filters = validateFilterString(self.filters) + + items = StockItem.objects.filter(**filters) + + items = items.filter(pk=item.pk) + + return items.exists() diff --git a/InvenTree/label/tests.py b/InvenTree/label/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/InvenTree/label/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/InvenTree/label/views.py b/InvenTree/label/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/InvenTree/label/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/Makefile b/Makefile index 0072e1ad9c..cc3a3043a6 100644 --- a/Makefile +++ b/Makefile @@ -51,12 +51,12 @@ style: # Run unit tests test: cd InvenTree && python3 manage.py check - cd InvenTree && python3 manage.py test barcode build common company order part report stock InvenTree + cd InvenTree && python3 manage.py test barcode build common company label order part report stock InvenTree # Run code coverage coverage: cd InvenTree && python3 manage.py check - coverage run InvenTree/manage.py test barcode build common company order part report stock InvenTree + coverage run InvenTree/manage.py test barcode build common company label order part report stock InvenTree coverage html # Install packages required to generate code docs