mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Merge pull request #906 from SchrodingersGat/blabel
Label Printing Functionality
This commit is contained in:
commit
b5b882d3b6
@ -242,7 +242,7 @@ def WrapWithQuotes(text, quote='"'):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def MakeBarcode(object_name, object_data):
|
def MakeBarcode(object_name, object_pk, object_data, **kwargs):
|
||||||
""" Generate a string for a barcode. Adds some global InvenTree parameters.
|
""" Generate a string for a barcode. Adds some global InvenTree parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -255,12 +255,20 @@ def MakeBarcode(object_name, object_data):
|
|||||||
json string of the supplied data plus some other data
|
json string of the supplied data plus some other data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = {
|
brief = kwargs.get('brief', False)
|
||||||
'tool': 'InvenTree',
|
|
||||||
'version': inventreeVersion(),
|
data = {}
|
||||||
'instance': inventreeInstanceName(),
|
|
||||||
object_name: object_data
|
if brief:
|
||||||
}
|
data[object_name] = object_pk
|
||||||
|
else:
|
||||||
|
data['tool'] = 'InvenTree'
|
||||||
|
data['version'] = inventreeVersion()
|
||||||
|
data['instance'] = inventreeInstanceName()
|
||||||
|
|
||||||
|
# Ensure PK is included
|
||||||
|
object_data['id'] = object_pk
|
||||||
|
data[object_name] = object_data
|
||||||
|
|
||||||
return json.dumps(data, sort_keys=True)
|
return json.dumps(data, sort_keys=True)
|
||||||
|
|
||||||
@ -383,3 +391,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))])
|
raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})".format(s=len(numbers), q=expected_quantity))])
|
||||||
|
|
||||||
return numbers
|
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
|
||||||
|
@ -130,6 +130,7 @@ INSTALLED_APPS = [
|
|||||||
'build.apps.BuildConfig',
|
'build.apps.BuildConfig',
|
||||||
'common.apps.CommonConfig',
|
'common.apps.CommonConfig',
|
||||||
'company.apps.CompanyConfig',
|
'company.apps.CompanyConfig',
|
||||||
|
'label.apps.LabelConfig',
|
||||||
'order.apps.OrderConfig',
|
'order.apps.OrderConfig',
|
||||||
'part.apps.PartConfig',
|
'part.apps.PartConfig',
|
||||||
'report.apps.ReportConfig',
|
'report.apps.ReportConfig',
|
||||||
|
@ -138,6 +138,7 @@ class TestMakeBarcode(TestCase):
|
|||||||
|
|
||||||
bc = helpers.MakeBarcode(
|
bc = helpers.MakeBarcode(
|
||||||
"part",
|
"part",
|
||||||
|
3,
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"url": "www.google.com",
|
"url": "www.google.com",
|
||||||
|
@ -47,12 +47,12 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for key in ['tool', 'version']:
|
# If any of the following keys are in the JSON data,
|
||||||
if key not in self.data.keys():
|
# let's go ahead and assume that the code is a valid InvenTree one...
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.data['tool'] == 'InvenTree':
|
for key in ['tool', 'version', 'InvenTree', 'stockitem', 'location', 'part']:
|
||||||
return False
|
if key in self.data.keys():
|
||||||
|
return True
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -60,10 +60,22 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
|
|
||||||
for k in self.data.keys():
|
for k in self.data.keys():
|
||||||
if k.lower() == 'stockitem':
|
if k.lower() == 'stockitem':
|
||||||
|
|
||||||
|
data = self.data[k]
|
||||||
|
|
||||||
|
pk = None
|
||||||
|
|
||||||
|
# Initially try casting to an integer
|
||||||
try:
|
try:
|
||||||
pk = self.data[k]['id']
|
pk = int(data)
|
||||||
except (AttributeError, KeyError):
|
except (TypeError, ValueError):
|
||||||
raise ValidationError({k: "id parameter not supplied"})
|
pk = None
|
||||||
|
|
||||||
|
if pk is None:
|
||||||
|
try:
|
||||||
|
pk = self.data[k]['id']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
raise ValidationError({k: "id parameter not supplied"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item = StockItem.objects.get(pk=pk)
|
item = StockItem.objects.get(pk=pk)
|
||||||
@ -77,10 +89,21 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
|
|
||||||
for k in self.data.keys():
|
for k in self.data.keys():
|
||||||
if k.lower() == 'stocklocation':
|
if k.lower() == 'stocklocation':
|
||||||
|
|
||||||
|
pk = None
|
||||||
|
|
||||||
|
# First try simple integer lookup
|
||||||
try:
|
try:
|
||||||
pk = self.data[k]['id']
|
pk = int(self.data[k])
|
||||||
except (AttributeError, KeyError):
|
except (TypeError, ValueError):
|
||||||
raise ValidationError({k: "id parameter not supplied"})
|
pk = None
|
||||||
|
|
||||||
|
if pk is None:
|
||||||
|
# Lookup by 'id' field
|
||||||
|
try:
|
||||||
|
pk = self.data[k]['id']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
raise ValidationError({k: "id parameter not supplied"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loc = StockLocation.objects.get(pk=pk)
|
loc = StockLocation.objects.get(pk=pk)
|
||||||
@ -94,10 +117,20 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
|
|
||||||
for k in self.data.keys():
|
for k in self.data.keys():
|
||||||
if k.lower() == 'part':
|
if k.lower() == 'part':
|
||||||
|
|
||||||
|
pk = None
|
||||||
|
|
||||||
|
# Try integer lookup first
|
||||||
try:
|
try:
|
||||||
pk = self.data[k]['id']
|
pk = int(self.data[k])
|
||||||
except (AttributeError, KeyError):
|
except (TypeError, ValueError):
|
||||||
raise ValidationError({k, 'id parameter not supplied'})
|
pk = None
|
||||||
|
|
||||||
|
if pk is None:
|
||||||
|
try:
|
||||||
|
pk = self.data[k]['id']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
raise ValidationError({k, 'id parameter not supplied'})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
part = Part.objects.get(pk=pk)
|
part = Part.objects.get(pk=pk)
|
||||||
|
0
InvenTree/label/__init__.py
Normal file
0
InvenTree/label/__init__.py
Normal file
14
InvenTree/label/admin.py
Normal file
14
InvenTree/label/admin.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import StockItemLabel
|
||||||
|
|
||||||
|
|
||||||
|
class StockItemLabelAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = ('name', 'description', 'label')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(StockItemLabel, StockItemLabelAdmin)
|
5
InvenTree/label/apps.py
Normal file
5
InvenTree/label/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LabelConfig(AppConfig):
|
||||||
|
name = 'label'
|
30
InvenTree/label/migrations/0001_initial.py
Normal file
30
InvenTree/label/migrations/0001_initial.py
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
InvenTree/label/migrations/__init__.py
Normal file
0
InvenTree/label/migrations/__init__.py
Normal file
149
InvenTree/label/models.py
Normal file
149
InvenTree/label/models.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
"""
|
||||||
|
Label printing models
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
|
||||||
|
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, normalize
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def template(self):
|
||||||
|
return self.label.path
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{n} - {d}".format(
|
||||||
|
n=self.name,
|
||||||
|
d=self.description
|
||||||
|
)
|
||||||
|
|
||||||
|
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 a list of dict objects, one for each item.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def render_to_file(self, filename, items, **kwargs):
|
||||||
|
"""
|
||||||
|
Render labels to a PDF file
|
||||||
|
"""
|
||||||
|
|
||||||
|
records = self.get_record_data(items)
|
||||||
|
|
||||||
|
writer = LabelWriter(self.template)
|
||||||
|
|
||||||
|
writer.write_labels(records, filename)
|
||||||
|
|
||||||
|
def render(self, items, **kwargs):
|
||||||
|
"""
|
||||||
|
Render labels to an in-memory PDF object, and return it
|
||||||
|
"""
|
||||||
|
|
||||||
|
records = self.get_record_data(items)
|
||||||
|
|
||||||
|
writer = LabelWriter(self.template)
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
|
||||||
|
writer.write_labels(records, buffer)
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def get_record_data(self, items):
|
||||||
|
"""
|
||||||
|
Generate context data for each provided StockItem
|
||||||
|
"""
|
||||||
|
records = []
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
|
||||||
|
# Add some basic information
|
||||||
|
records.append({
|
||||||
|
'item': item,
|
||||||
|
'part': item.part,
|
||||||
|
'name': item.part.name,
|
||||||
|
'ipn': item.part.IPN,
|
||||||
|
'quantity': normalize(item.quantity),
|
||||||
|
'serial': item.serial,
|
||||||
|
'uid': item.uid,
|
||||||
|
'pk': item.pk,
|
||||||
|
'qr_data': item.format_barcode(brief=True),
|
||||||
|
'tests': item.testResultMap()
|
||||||
|
})
|
||||||
|
|
||||||
|
return records
|
1
InvenTree/label/tests.py
Normal file
1
InvenTree/label/tests.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Create your tests here.
|
1
InvenTree/label/views.py
Normal file
1
InvenTree/label/views.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Create your views here.
|
Binary file not shown.
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
@ -560,16 +560,17 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name='parts_responible')
|
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name='parts_responible')
|
||||||
|
|
||||||
def format_barcode(self):
|
def format_barcode(self, **kwargs):
|
||||||
""" Return a JSON string for formatting a barcode for this Part object """
|
""" Return a JSON string for formatting a barcode for this Part object """
|
||||||
|
|
||||||
return helpers.MakeBarcode(
|
return helpers.MakeBarcode(
|
||||||
"part",
|
"part",
|
||||||
|
self.id,
|
||||||
{
|
{
|
||||||
"id": self.id,
|
|
||||||
"name": self.full_name,
|
"name": self.full_name,
|
||||||
"url": reverse('api-part-detail', kwargs={'pk': self.id}),
|
"url": reverse('api-part-detail', kwargs={'pk': self.id}),
|
||||||
}
|
},
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -178,6 +178,37 @@ class SerializeStockForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StockItemLabelSelectForm(HelperForm):
|
||||||
|
""" Form for selecting a label template for a StockItem """
|
||||||
|
|
||||||
|
label = forms.ChoiceField(
|
||||||
|
label=_('Label'),
|
||||||
|
help_text=_('Select test report template')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = StockItem
|
||||||
|
fields = [
|
||||||
|
'label',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_label_choices(self, labels):
|
||||||
|
|
||||||
|
choices = []
|
||||||
|
|
||||||
|
if len(labels) > 0:
|
||||||
|
for label in labels:
|
||||||
|
choices.append((label.pk, label))
|
||||||
|
|
||||||
|
return choices
|
||||||
|
|
||||||
|
def __init__(self, labels, *args, **kwargs):
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['label'].choices = self.get_label_choices(labels)
|
||||||
|
|
||||||
|
|
||||||
class TestReportFormatForm(HelperForm):
|
class TestReportFormatForm(HelperForm):
|
||||||
""" Form for selection a test report template """
|
""" Form for selection a test report template """
|
||||||
|
|
||||||
|
@ -45,16 +45,17 @@ class StockLocation(InvenTreeTree):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
def format_barcode(self):
|
def format_barcode(self, **kwargs):
|
||||||
""" Return a JSON string for formatting a barcode for this StockLocation object """
|
""" Return a JSON string for formatting a barcode for this StockLocation object """
|
||||||
|
|
||||||
return helpers.MakeBarcode(
|
return helpers.MakeBarcode(
|
||||||
'stocklocation',
|
'stocklocation',
|
||||||
|
self.pk,
|
||||||
{
|
{
|
||||||
"id": self.id,
|
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"url": reverse('api-location-detail', kwargs={'pk': self.id}),
|
"url": reverse('api-location-detail', kwargs={'pk': self.id}),
|
||||||
}
|
},
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_stock_items(self, cascade=True):
|
def get_stock_items(self, cascade=True):
|
||||||
@ -283,7 +284,7 @@ class StockItem(MPTTModel):
|
|||||||
def get_part_name(self):
|
def get_part_name(self):
|
||||||
return self.part.full_name
|
return self.part.full_name
|
||||||
|
|
||||||
def format_barcode(self):
|
def format_barcode(self, **kwargs):
|
||||||
""" Return a JSON string for formatting a barcode for this StockItem.
|
""" Return a JSON string for formatting a barcode for this StockItem.
|
||||||
Can be used to perform lookup of a stockitem using barcode
|
Can be used to perform lookup of a stockitem using barcode
|
||||||
|
|
||||||
@ -296,10 +297,11 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
return helpers.MakeBarcode(
|
return helpers.MakeBarcode(
|
||||||
"stockitem",
|
"stockitem",
|
||||||
|
self.id,
|
||||||
{
|
{
|
||||||
"id": self.id,
|
|
||||||
"url": reverse('api-stock-detail', kwargs={'pk': self.id}),
|
"url": reverse('api-stock-detail', kwargs={'pk': self.id}),
|
||||||
}
|
},
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
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"))
|
||||||
|
@ -78,7 +78,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
<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>
|
||||||
{% if item.uid %}
|
{% if item.uid %}
|
||||||
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
|
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -126,7 +126,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
</div>
|
</div>
|
||||||
{% if item.part.has_test_report_templates %}
|
{% if item.part.has_test_report_templates %}
|
||||||
<button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'>
|
<button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'>
|
||||||
<span class='fas fa-tasks'/>
|
<span class='fas fa-file-invoice'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -314,6 +314,15 @@ $("#stock-test-report").click(function() {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
$("#print-label").click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'stock-item-label-select' item.id %}",
|
||||||
|
{
|
||||||
|
follow: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
$("#stock-duplicate").click(function() {
|
$("#stock-duplicate").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'stock-item-create' %}",
|
"{% url 'stock-item-create' %}",
|
||||||
|
@ -29,6 +29,7 @@ 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'),
|
||||||
@ -59,6 +60,7 @@ stock_urls = [
|
|||||||
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
|
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
|
||||||
|
|
||||||
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([
|
||||||
|
@ -28,6 +28,7 @@ from datetime import datetime
|
|||||||
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
|
||||||
|
|
||||||
from .admin import StockItemResource
|
from .admin import StockItemResource
|
||||||
@ -295,6 +296,88 @@ class StockItemReturnToStock(AjaxUpdateView):
|
|||||||
return self.renderJsonResponse(request, self.get_form(), data)
|
return self.renderJsonResponse(request, self.get_form(), data)
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
|
||||||
|
item = StockItem.objects.get(pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
for label in StockItemLabel.objects.all():
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
4
Makefile
4
Makefile
@ -51,12 +51,12 @@ style:
|
|||||||
# Run unit tests
|
# Run unit tests
|
||||||
test:
|
test:
|
||||||
cd InvenTree && python3 manage.py check
|
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
|
# Run code coverage
|
||||||
coverage:
|
coverage:
|
||||||
cd InvenTree && python3 manage.py check
|
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
|
coverage html
|
||||||
|
|
||||||
# Install packages required to generate code docs
|
# Install packages required to generate code docs
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
wheel>=0.34.2 # Wheel
|
wheel>=0.34.2 # Wheel
|
||||||
Django==3.0.7 # Django package
|
Django==3.0.7 # Django package
|
||||||
pillow==7.1.0 # Image manipulation
|
pillow==7.1.0 # Image manipulation
|
||||||
|
blabel==0.1.3 # Simple PDF label printing
|
||||||
djangorestframework==3.10.3 # DRF framework
|
djangorestframework==3.10.3 # DRF framework
|
||||||
django-dbbackup==3.3.0 # Database backup / restore functionality
|
django-dbbackup==3.3.0 # Database backup / restore functionality
|
||||||
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
||||||
|
Loading…
x
Reference in New Issue
Block a user