mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
* Adjust default values for test result fields * Add helper functions: - current_time() - current_date() Handles timezone "awareness" * Use new helper function widely * Update defaults - do not use None * Allow null field values
430 lines
13 KiB
Python
430 lines
13 KiB
Python
"""Label printing models."""
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import User
|
|
from django.core.validators import FileExtensionValidator, MinValueValidator
|
|
from django.db import models
|
|
from django.template import Context, Template
|
|
from django.template.loader import render_to_string
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
import build.models
|
|
import InvenTree.helpers
|
|
import InvenTree.models
|
|
import part.models
|
|
import stock.models
|
|
from InvenTree.helpers import normalize, validateFilterString
|
|
from InvenTree.helpers_model import get_base_url
|
|
from plugin.registry import registry
|
|
|
|
try:
|
|
from django_weasyprint import WeasyTemplateResponseMixin
|
|
except OSError as err: # pragma: no cover
|
|
print(f'OSError: {err}')
|
|
print('You may require some further system packages to be installed.')
|
|
sys.exit(1)
|
|
|
|
|
|
logger = logging.getLogger('inventree')
|
|
|
|
|
|
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)
|
|
|
|
|
|
def rename_label_output(instance, filename):
|
|
"""Place the label output file into the correct subdirectory."""
|
|
filename = os.path.basename(filename)
|
|
|
|
return os.path.join('label', 'output', filename)
|
|
|
|
|
|
def validate_stock_item_filters(filters):
|
|
"""Validate query filters for the StockItemLabel model."""
|
|
filters = validateFilterString(filters, model=stock.models.StockItem)
|
|
|
|
return filters
|
|
|
|
|
|
def validate_stock_location_filters(filters):
|
|
"""Validate query filters for the StockLocationLabel model."""
|
|
filters = validateFilterString(filters, model=stock.models.StockLocation)
|
|
|
|
return filters
|
|
|
|
|
|
def validate_part_filters(filters):
|
|
"""Validate query filters for the PartLabel model."""
|
|
filters = validateFilterString(filters, model=part.models.Part)
|
|
|
|
return filters
|
|
|
|
|
|
def validate_build_line_filters(filters):
|
|
"""Validate query filters for the BuildLine model."""
|
|
filters = validateFilterString(filters, model=build.models.BuildLine)
|
|
|
|
return filters
|
|
|
|
|
|
class WeasyprintLabelMixin(WeasyTemplateResponseMixin):
|
|
"""Class for rendering a label to a PDF."""
|
|
|
|
pdf_filename = 'label.pdf'
|
|
pdf_attachment = True
|
|
|
|
def __init__(self, request, template, **kwargs):
|
|
"""Initialize a label mixin with certain properties."""
|
|
self.request = request
|
|
self.template_name = template
|
|
self.pdf_filename = kwargs.get('filename', 'label.pdf')
|
|
|
|
|
|
class LabelTemplate(InvenTree.models.InvenTreeMetadataModel):
|
|
"""Base class for generic, filterable labels."""
|
|
|
|
class Meta:
|
|
"""Metaclass options. Abstract ensures no database table is created."""
|
|
|
|
abstract = True
|
|
|
|
@classmethod
|
|
def getSubdir(cls) -> str:
|
|
"""Return the subdirectory for this label."""
|
|
return cls.SUBDIR
|
|
|
|
# Each class of label files will be stored in a separate subdirectory
|
|
SUBDIR: str = 'label'
|
|
|
|
# Object we will be printing against (will be filled out later)
|
|
object_to_print = None
|
|
|
|
@property
|
|
def template(self):
|
|
"""Return the file path of the template associated with this label instance."""
|
|
return self.label.path
|
|
|
|
def __str__(self):
|
|
"""Format a string representation of a label instance."""
|
|
return f'{self.name} - {self.description}'
|
|
|
|
name = models.CharField(
|
|
blank=False, max_length=100, verbose_name=_('Name'), help_text=_('Label name')
|
|
)
|
|
|
|
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'])],
|
|
)
|
|
|
|
enabled = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_('Enabled'),
|
|
help_text=_('Label template is enabled'),
|
|
)
|
|
|
|
width = models.FloatField(
|
|
default=50,
|
|
verbose_name=_('Width [mm]'),
|
|
help_text=_('Label width, specified in mm'),
|
|
validators=[MinValueValidator(2)],
|
|
)
|
|
|
|
height = models.FloatField(
|
|
default=20,
|
|
verbose_name=_('Height [mm]'),
|
|
help_text=_('Label height, specified in mm'),
|
|
validators=[MinValueValidator(2)],
|
|
)
|
|
|
|
filename_pattern = models.CharField(
|
|
default='label.pdf',
|
|
verbose_name=_('Filename Pattern'),
|
|
help_text=_('Pattern for generating label filenames'),
|
|
max_length=100,
|
|
)
|
|
|
|
@property
|
|
def template_name(self):
|
|
"""Returns the file system path to the template file.
|
|
|
|
Required for passing the file to an external process
|
|
"""
|
|
template = self.label.name
|
|
template = template.replace('/', os.path.sep)
|
|
template = template.replace('\\', os.path.sep)
|
|
|
|
template = settings.MEDIA_ROOT.joinpath(template)
|
|
|
|
return template
|
|
|
|
def get_context_data(self, request):
|
|
"""Supply custom context data to the template for rendering.
|
|
|
|
Note: Override this in any subclass
|
|
"""
|
|
return {} # pragma: no cover
|
|
|
|
def generate_filename(self, request, **kwargs):
|
|
"""Generate a filename for this label."""
|
|
template_string = Template(self.filename_pattern)
|
|
|
|
ctx = self.context(request)
|
|
|
|
context = Context(ctx)
|
|
|
|
return template_string.render(context)
|
|
|
|
def generate_page_style(self, **kwargs):
|
|
"""Generate @page style for the label template.
|
|
|
|
This is inserted at the top of the style block for a given label
|
|
"""
|
|
width = kwargs.get('width', self.width)
|
|
height = kwargs.get('height', self.height)
|
|
margin = kwargs.get('margin', 0)
|
|
|
|
return f"""
|
|
@page {{
|
|
size: {width}mm {height}mm;
|
|
margin: {margin}mm;
|
|
}}
|
|
"""
|
|
|
|
def context(self, request, **kwargs):
|
|
"""Provides context data to the template.
|
|
|
|
Arguments:
|
|
request: The HTTP request object
|
|
kwargs: Additional keyword arguments
|
|
"""
|
|
context = self.get_context_data(request)
|
|
|
|
# By default, each label is supplied with '@page' data
|
|
# However, it can be excluded, e.g. when rendering a label sheet
|
|
if kwargs.get('insert_page_style', True):
|
|
context['page_style'] = self.generate_page_style()
|
|
|
|
# Add "basic" context data which gets passed to every label
|
|
context['base_url'] = get_base_url(request=request)
|
|
context['date'] = InvenTree.helpers.current_date()
|
|
context['datetime'] = InvenTree.helpers.current_time()
|
|
context['request'] = request
|
|
context['user'] = request.user
|
|
context['width'] = self.width
|
|
context['height'] = self.height
|
|
|
|
# Pass the context through to any registered plugins
|
|
plugins = registry.with_mixin('report')
|
|
|
|
for plugin in plugins:
|
|
# Let each plugin add its own context data
|
|
plugin.add_label_context(self, self.object_to_print, request, context)
|
|
|
|
return context
|
|
|
|
def render_as_string(self, request, target_object=None, **kwargs):
|
|
"""Render the label to a HTML string."""
|
|
if target_object:
|
|
self.object_to_print = target_object
|
|
|
|
context = self.context(request, **kwargs)
|
|
|
|
return render_to_string(self.template_name, context, request)
|
|
|
|
def render(self, request, target_object=None, **kwargs):
|
|
"""Render the label template to a PDF file.
|
|
|
|
Uses django-weasyprint plugin to render HTML template
|
|
"""
|
|
if target_object:
|
|
self.object_to_print = target_object
|
|
|
|
context = self.context(request, **kwargs)
|
|
|
|
wp = WeasyprintLabelMixin(
|
|
request,
|
|
self.template_name,
|
|
base_url=request.build_absolute_uri('/'),
|
|
presentational_hints=True,
|
|
filename=self.generate_filename(request),
|
|
**kwargs,
|
|
)
|
|
|
|
return wp.render_to_response(context, **kwargs)
|
|
|
|
|
|
class LabelOutput(models.Model):
|
|
"""Class representing a label output file.
|
|
|
|
'Printing' a label may generate a file object (such as PDF)
|
|
which is made available for download.
|
|
|
|
Future work will offload this task to the background worker,
|
|
and provide a 'progress' bar for the user.
|
|
"""
|
|
|
|
# File will be stored in a subdirectory
|
|
label = models.FileField(
|
|
upload_to=rename_label_output, unique=True, blank=False, null=False
|
|
)
|
|
|
|
# Creation date of label output
|
|
created = models.DateField(auto_now_add=True, editable=False)
|
|
|
|
# User who generated the label
|
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
|
|
|
|
|
|
class StockItemLabel(LabelTemplate):
|
|
"""Template for printing StockItem labels."""
|
|
|
|
@staticmethod
|
|
def get_api_url():
|
|
"""Return the API URL associated with the StockItemLabel model."""
|
|
return reverse('api-stockitem-label-list') # pragma: no cover
|
|
|
|
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 get_context_data(self, request):
|
|
"""Generate context data for each provided StockItem."""
|
|
stock_item = self.object_to_print
|
|
|
|
return {
|
|
'item': stock_item,
|
|
'part': stock_item.part,
|
|
'name': stock_item.part.full_name,
|
|
'ipn': stock_item.part.IPN,
|
|
'revision': stock_item.part.revision,
|
|
'quantity': normalize(stock_item.quantity),
|
|
'serial': stock_item.serial,
|
|
'barcode_data': stock_item.barcode_data,
|
|
'barcode_hash': stock_item.barcode_hash,
|
|
'qr_data': stock_item.format_barcode(brief=True),
|
|
'qr_url': request.build_absolute_uri(stock_item.get_absolute_url()),
|
|
'tests': stock_item.testResultMap(),
|
|
'parameters': stock_item.part.parameters_map(),
|
|
}
|
|
|
|
|
|
class StockLocationLabel(LabelTemplate):
|
|
"""Template for printing StockLocation labels."""
|
|
|
|
@staticmethod
|
|
def get_api_url():
|
|
"""Return the API URL associated with the StockLocationLabel model."""
|
|
return reverse('api-stocklocation-label-list') # pragma: no cover
|
|
|
|
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 get_context_data(self, request):
|
|
"""Generate context data for each provided StockLocation."""
|
|
location = self.object_to_print
|
|
|
|
return {'location': location, 'qr_data': location.format_barcode(brief=True)}
|
|
|
|
|
|
class PartLabel(LabelTemplate):
|
|
"""Template for printing Part labels."""
|
|
|
|
@staticmethod
|
|
def get_api_url():
|
|
"""Return the API url associated with the PartLabel model."""
|
|
return reverse('api-part-label-list') # pragma: no cover
|
|
|
|
SUBDIR = 'part'
|
|
|
|
filters = models.CharField(
|
|
blank=True,
|
|
max_length=250,
|
|
help_text=_('Query filters (comma-separated list of key=value pairs)'),
|
|
verbose_name=_('Filters'),
|
|
validators=[validate_part_filters],
|
|
)
|
|
|
|
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': request.build_absolute_uri(part.get_absolute_url()),
|
|
'parameters': part.parameters_map(),
|
|
}
|
|
|
|
|
|
class BuildLineLabel(LabelTemplate):
|
|
"""Template for printing labels against BuildLine objects."""
|
|
|
|
@staticmethod
|
|
def get_api_url():
|
|
"""Return the API URL associated with the BuildLineLabel model."""
|
|
return reverse('api-buildline-label-list')
|
|
|
|
SUBDIR = 'buildline'
|
|
|
|
filters = models.CharField(
|
|
blank=True,
|
|
max_length=250,
|
|
help_text=_('Query filters (comma-separated list of key=value pairs)'),
|
|
verbose_name=_('Filters'),
|
|
validators=[validate_build_line_filters],
|
|
)
|
|
|
|
def get_context_data(self, request):
|
|
"""Generate context data for each provided BuildLine object."""
|
|
build_line = self.object_to_print
|
|
|
|
return {
|
|
'build_line': build_line,
|
|
'build': build_line.build,
|
|
'bom_item': build_line.bom_item,
|
|
'part': build_line.bom_item.sub_part,
|
|
'quantity': build_line.quantity,
|
|
'allocated_quantity': build_line.allocated_quantity,
|
|
'allocations': build_line.allocations,
|
|
}
|