initial implementation of barcode generation using plugins

This commit is contained in:
wolflu05 2024-07-14 13:22:04 +02:00
parent 0db280ad74
commit 24bd5d0962
No known key found for this signature in database
GPG Key ID: 9099EFC7C5EB963C
16 changed files with 276 additions and 67 deletions

View File

@ -396,38 +396,6 @@ def WrapWithQuotes(text, quote='"'):
return text return text
def MakeBarcode(cls_name, object_pk: int, object_data=None, **kwargs):
"""Generate a string for a barcode. Adds some global InvenTree parameters.
Args:
cls_name: string describing the object type e.g. 'StockItem'
object_pk (int): ID (Primary Key) of the object in the database
object_data: Python dict object containing extra data which will be rendered to string (must only contain stringable values)
Returns:
json string of the supplied data plus some other data
"""
if object_data is None:
object_data = {}
brief = kwargs.get('brief', True)
data = {}
if brief:
data[cls_name] = object_pk
else:
data['tool'] = 'InvenTree'
data['version'] = InvenTree.version.inventreeVersion()
data['instance'] = InvenTree.version.inventreeInstanceName()
# Ensure PK is included
object_data['id'] = object_pk
data[cls_name] = object_data
return str(json.dumps(data, sort_keys=True))
def GetExportFormats(): def GetExportFormats():
"""Return a list of allowable file formats for importing or exporting tabular data.""" """Return a list of allowable file formats for importing or exporting tabular data."""
return ['csv', 'xlsx', 'tsv', 'json'] return ['csv', 'xlsx', 'tsv', 'json']

View File

@ -15,9 +15,6 @@ from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money from djmoney.money import Money
from PIL import Image from PIL import Image
import InvenTree
import InvenTree.helpers_model
import InvenTree.version
from common.notifications import ( from common.notifications import (
InvenTreeNotificationBodies, InvenTreeNotificationBodies,
NotificationBody, NotificationBody,
@ -331,9 +328,7 @@ def notify_users(
'instance': instance, 'instance': instance,
'name': content.name.format(**content_context), 'name': content.name.format(**content_context),
'message': content.message.format(**content_context), 'message': content.message.format(**content_context),
'link': InvenTree.helpers_model.construct_absolute_url( 'link': construct_absolute_url(instance.get_absolute_url()),
instance.get_absolute_url()
),
'template': {'subject': content.name.format(**content_context)}, 'template': {'subject': content.name.format(**content_context)},
} }

View File

@ -3,9 +3,7 @@
import logging import logging
from datetime import datetime from datetime import datetime
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
@ -934,6 +932,8 @@ class InvenTreeBarcodeMixin(models.Model):
- barcode_data : Raw data associated with an assigned barcode - barcode_data : Raw data associated with an assigned barcode
- barcode_hash : A 'hash' of the assigned barcode data used to improve matching - barcode_hash : A 'hash' of the assigned barcode data used to improve matching
The barcode_model_type_code() classmethod must be implemented in the model class.
""" """
class Meta: class Meta:
@ -964,11 +964,25 @@ class InvenTreeBarcodeMixin(models.Model):
# By default, use the name of the class # By default, use the name of the class
return cls.__name__.lower() return cls.__name__.lower()
@classmethod
def barcode_model_type_code(cls):
r"""Return a 'short' code for the model type.
This is used to generate a efficient QR code for the model type.
It is expected to match this pattern: [0-9A-Z $%*+-.\/:]{2}
Note: Due to the shape constrains (45**2=2025 different allowed codes)
this needs to be explicitly implemented in the model class to avoid collisions.
"""
raise NotImplementedError(
'barcode_model_type_code() must be implemented in the model class'
)
def format_barcode(self, **kwargs): def format_barcode(self, **kwargs):
"""Return a JSON string for formatting a QR code for this model instance.""" """Return a JSON string for formatting a QR code for this model instance."""
return InvenTree.helpers.MakeBarcode( from plugin.base.barcodes.helper import generate_barcode
self.__class__.barcode_model_type(), self.pk, **kwargs
) return generate_barcode(self)
def format_matched_response(self): def format_matched_response(self):
"""Format a standard response for a matched barcode.""" """Format a standard response for a matched barcode."""
@ -986,7 +1000,7 @@ class InvenTreeBarcodeMixin(models.Model):
@property @property
def barcode(self): def barcode(self):
"""Format a minimal barcode string (e.g. for label printing).""" """Format a minimal barcode string (e.g. for label printing)."""
return self.format_barcode(brief=True) return self.format_barcode()
@classmethod @classmethod
def lookup_barcode(cls, barcode_hash): def lookup_barcode(cls, barcode_hash):

View File

@ -115,6 +115,11 @@ class Build(
return defaults return defaults
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return "BO"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Custom save method for the BuildOrder model""" """Custom save method for the BuildOrder model"""
self.validate_reference_field(self.reference) self.validate_reference_field(self.reference)

View File

@ -9,12 +9,13 @@ import hmac
import json import json
import logging import logging
import os import os
import sys
import uuid import uuid
from datetime import timedelta, timezone from datetime import timedelta, timezone
from enum import Enum from enum import Enum
from io import BytesIO from io import BytesIO
from secrets import compare_digest from secrets import compare_digest
from typing import Any, Callable, Collection, TypedDict, Union from typing import Any, Callable, TypedDict, Union
from django.apps import apps from django.apps import apps
from django.conf import settings as django_settings from django.conf import settings as django_settings
@ -49,6 +50,7 @@ import InvenTree.ready
import InvenTree.tasks import InvenTree.tasks
import InvenTree.validators import InvenTree.validators
import order.validators import order.validators
import plugin.base.barcodes.helper
import report.helpers import report.helpers
import users.models import users.models
from InvenTree.sanitizer import sanitize_svg from InvenTree.sanitizer import sanitize_svg
@ -56,6 +58,13 @@ from plugin import registry
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
if sys.version_info >= (3, 11):
from typing import NotRequired
else:
class NotRequired:
"""NotRequired type helper is only supported with Python 3.11+."""
class MetaMixin(models.Model): class MetaMixin(models.Model):
"""A base class for InvenTree models to include shared meta fields. """A base class for InvenTree models to include shared meta fields.
@ -1167,7 +1176,7 @@ class InvenTreeSettingsKeyType(SettingsKeyType):
requires_restart: If True, a server restart is required after changing the setting requires_restart: If True, a server restart is required after changing the setting
""" """
requires_restart: bool requires_restart: NotRequired[bool]
class InvenTreeSetting(BaseInvenTreeSetting): class InvenTreeSetting(BaseInvenTreeSetting):
@ -1402,6 +1411,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'BARCODE_GENERATION_PLUGIN': {
'name': _('Barcode Generation Plugin'),
'description': _('Plugin to use for internal barcode data generation'),
'choices': plugin.base.barcodes.helper.barcode_plugins,
'default': 'inventreebarcode',
},
'PART_ENABLE_REVISION': { 'PART_ENABLE_REVISION': {
'name': _('Part Revisions'), 'name': _('Part Revisions'),
'description': _('Enable revision field for Part'), 'description': _('Enable revision field for Part'),

View File

@ -474,6 +474,11 @@ class ManufacturerPart(
"""Return the API URL associated with the ManufacturerPart instance.""" """Return the API URL associated with the ManufacturerPart instance."""
return reverse('api-manufacturer-part-list') return reverse('api-manufacturer-part-list')
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'MP'
part = models.ForeignKey( part = models.ForeignKey(
'part.Part', 'part.Part',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -676,6 +681,11 @@ class SupplierPart(
"""Return custom API filters for this particular instance.""" """Return custom API filters for this particular instance."""
return {'manufacturer_part': {'part': self.part.pk}} return {'manufacturer_part': {'part': self.part.pk}}
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'SP'
def clean(self): def clean(self):
"""Custom clean action for the SupplierPart model. """Custom clean action for the SupplierPart model.

View File

@ -408,6 +408,11 @@ class PurchaseOrder(TotalPriceMixin, Order):
return defaults return defaults
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'PO'
@staticmethod @staticmethod
def filterByDate(queryset, min_date, max_date): def filterByDate(queryset, min_date, max_date):
"""Filter by 'minimum and maximum date range'. """Filter by 'minimum and maximum date range'.
@ -871,6 +876,11 @@ class SalesOrder(TotalPriceMixin, Order):
return defaults return defaults
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'SO'
@staticmethod @staticmethod
def filterByDate(queryset, min_date, max_date): def filterByDate(queryset, min_date, max_date):
"""Filter by "minimum and maximum date range". """Filter by "minimum and maximum date range".
@ -2035,6 +2045,11 @@ class ReturnOrder(TotalPriceMixin, Order):
return defaults return defaults
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'RO'
def __str__(self): def __str__(self):
"""Render a string representation of this ReturnOrder.""" """Render a string representation of this ReturnOrder."""
return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}" return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}"

View File

@ -416,6 +416,11 @@ class Part(
"""Return API query filters for limiting field results against this instance.""" """Return API query filters for limiting field results against this instance."""
return {'variant_of': {'exclude_tree': self.pk}} return {'variant_of': {'exclude_tree': self.pk}}
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'PA'
def report_context(self): def report_context(self):
"""Return custom report context information.""" """Return custom report context information."""
return { return {
@ -426,11 +431,11 @@ class Part(
'name': self.name, 'name': self.name,
'parameters': self.parameters_map(), 'parameters': self.parameters_map(),
'part': self, 'part': self,
'qr_data': self.format_barcode(brief=True), 'qr_data': self.barcode,
'qr_url': self.get_absolute_url(), 'qr_url': self.get_absolute_url(),
'revision': self.revision, 'revision': self.revision,
'test_template_list': self.getTestTemplates(), 'test_template_list': self.getTestTemplates(),
'test_templates': self.getTestTemplates(), 'test_templates': self.getTestTemplateMap(),
} }
def get_context_data(self, request, **kwargs): def get_context_data(self, request, **kwargs):

View File

@ -6,16 +6,17 @@ from django.db.models import F
from django.urls import path from django.urls import path
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import permissions from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import permissions, status
from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from rest_framework.response import Response from rest_framework.response import Response
import order.models import order.models
import plugin.base.barcodes.helper
import stock.models import stock.models
from InvenTree.helpers import hash_barcode from InvenTree.helpers import hash_barcode
from plugin import registry from plugin import registry
from plugin.builtin.barcodes.inventree_barcode import InvenTreeInternalBarcodePlugin
from users.models import RuleSet from users.models import RuleSet
from . import serializers as barcode_serializers from . import serializers as barcode_serializers
@ -129,6 +130,45 @@ class BarcodeScan(BarcodeView):
return Response(result) return Response(result)
@extend_schema_view(
post=extend_schema(responses={200: barcode_serializers.BarcodeSerializer})
)
class BarcodeGenerate(CreateAPIView):
"""Endpoint for generating a barcode for a database object.
The barcode is generated by the selected barcode plugin.
"""
serializer_class = barcode_serializers.BarcodeGenerateSerializer
def queryset(self):
"""This API view does not have a queryset."""
return None
# Default permission classes (can be overridden)
permission_classes = [permissions.IsAuthenticated]
def create(self, request, *args, **kwargs):
"""Perform the barcode generation action."""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
model = serializer.validated_data.get('model')
pk = serializer.validated_data.get('pk')
model_cls = plugin.base.barcodes.helper.get_supported_barcode_models_map().get(
model, None
)
if model_cls is None:
raise ValidationError({'error': _('Model is not supported')})
model_instance = model_cls.objects.get(pk=pk)
barcode_data = plugin.base.barcodes.helper.generate_barcode(model_instance)
return Response({'barcode': barcode_data}, status=status.HTTP_200_OK)
class BarcodeAssign(BarcodeView): class BarcodeAssign(BarcodeView):
"""Endpoint for assigning a barcode to a stock item. """Endpoint for assigning a barcode to a stock item.
@ -161,7 +201,7 @@ class BarcodeAssign(BarcodeView):
valid_labels = [] valid_labels = []
for model in InvenTreeInternalBarcodePlugin.get_supported_barcode_models(): for model in plugin.base.barcodes.helper.get_supported_barcode_models():
label = model.barcode_model_type() label = model.barcode_model_type()
valid_labels.append(label) valid_labels.append(label)
@ -203,7 +243,7 @@ class BarcodeUnassign(BarcodeView):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data data = serializer.validated_data
supported_models = InvenTreeInternalBarcodePlugin.get_supported_barcode_models() supported_models = plugin.base.barcodes.helper.get_supported_barcode_models()
supported_labels = [model.barcode_model_type() for model in supported_models] supported_labels = [model.barcode_model_type() for model in supported_models]
model_names = ', '.join(supported_labels) model_names = ', '.join(supported_labels)
@ -567,6 +607,8 @@ class BarcodeSOAllocate(BarcodeView):
barcode_api_urls = [ barcode_api_urls = [
# Generate a barcode for a database object
path('generate/', BarcodeGenerate.as_view(), name='api-barcode-generate'),
# Link a third-party barcode to an item (e.g. Part / StockItem / etc) # Link a third-party barcode to an item (e.g. Part / StockItem / etc)
path('link/', BarcodeAssign.as_view(), name='api-barcode-link'), path('link/', BarcodeAssign.as_view(), name='api-barcode-link'),
# Unlink a third-party barcode from an item # Unlink a third-party barcode from an item

View File

@ -0,0 +1,48 @@
"""Helper functions for barcode generation."""
import logging
from typing import Type
import InvenTree.helpers_model
from InvenTree.models import InvenTreeBarcodeMixin
logger = logging.getLogger('inventree')
def barcode_plugins() -> list:
"""Return a list of plugin choices which can be used for barcode generation."""
try:
from plugin import registry
plugins = registry.with_mixin('barcode', active=True)
except Exception:
plugins = []
return [
(plug.slug, plug.human_name) for plug in plugins if plug.has_barcode_generation
]
def generate_barcode(model_instance: InvenTreeBarcodeMixin):
"""Generate a barcode for a given model instance."""
from common.settings import get_global_setting
from plugin import registry
# Find the selected barcode generation plugin
slug = get_global_setting('BARCODE_GENERATION_PLUGIN', create=False)
plugin = registry.get_plugin(slug)
return plugin.generate(model_instance)
def get_supported_barcode_models() -> list[Type[InvenTreeBarcodeMixin]]:
"""Returns a list of database models which support barcode functionality."""
return InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin)
def get_supported_barcode_models_map():
"""Return a mapping of barcode model types to the model class."""
return {
model.barcode_model_type(): model for model in get_supported_barcode_models()
}

View File

@ -10,6 +10,7 @@ from django.db.models import F, Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from InvenTree.models import InvenTreeBarcodeMixin
from order.models import PurchaseOrder, PurchaseOrderStatus from order.models import PurchaseOrder, PurchaseOrderStatus
from plugin.base.integration.SettingsMixin import SettingsMixin from plugin.base.integration.SettingsMixin import SettingsMixin
from stock.models import StockLocation from stock.models import StockLocation
@ -53,6 +54,30 @@ class BarcodeMixin:
""" """
return None return None
@property
def has_barcode_generation(self):
"""Does this plugin support barcode generation."""
try:
# Attempt to call the generate method
self.generate(None) # type: ignore
except NotImplementedError:
# If a NotImplementedError is raised, then barcode generation is not supported
return False
except:
pass
return True
def generate(self, model_instance: InvenTreeBarcodeMixin):
"""Generate barcode data for the given model instance.
Arguments:
model_instance: The model instance to generate barcode data for. It is extending the InvenTreeBarcodeMixin.
Returns: The generated barcode data.
"""
raise NotImplementedError('Generate must be implemented by a plugin')
class SupplierBarcodeMixin(BarcodeMixin): class SupplierBarcodeMixin(BarcodeMixin):
"""Mixin that provides default implementations for scan functions for supplier barcodes. """Mixin that provides default implementations for scan functions for supplier barcodes.

View File

@ -6,9 +6,9 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
import order.models import order.models
import plugin.base.barcodes.helper
import stock.models import stock.models
from order.status_codes import PurchaseOrderStatus, SalesOrderStatus from order.status_codes import PurchaseOrderStatus, SalesOrderStatus
from plugin.builtin.barcodes.inventree_barcode import InvenTreeInternalBarcodePlugin
class BarcodeSerializer(serializers.Serializer): class BarcodeSerializer(serializers.Serializer):
@ -23,6 +23,30 @@ class BarcodeSerializer(serializers.Serializer):
) )
class BarcodeGenerateSerializer(serializers.Serializer):
"""Serializer for generating a barcode."""
model = serializers.CharField(
required=True, help_text=_('Model name to generate barcode for')
)
pk = serializers.IntegerField(
required=True,
help_text=_('Primary key of model object to generate barcode for'),
)
def validate_model(self, model: str):
"""Validate the provided model."""
supported_models = (
plugin.base.barcodes.helper.get_supported_barcode_models_map()
)
if model not in supported_models.keys():
raise ValidationError(_('Model is not supported'))
return model
class BarcodeAssignMixin(serializers.Serializer): class BarcodeAssignMixin(serializers.Serializer):
"""Serializer for linking and unlinking barcode to an internal class.""" """Serializer for linking and unlinking barcode to an internal class."""
@ -30,7 +54,7 @@ class BarcodeAssignMixin(serializers.Serializer):
"""Generate serializer fields for each supported model type.""" """Generate serializer fields for each supported model type."""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for model in InvenTreeInternalBarcodePlugin.get_supported_barcode_models(): for model in plugin.base.barcodes.helper.get_supported_barcode_models():
self.fields[model.barcode_model_type()] = ( self.fields[model.barcode_model_type()] = (
serializers.PrimaryKeyRelatedField( serializers.PrimaryKeyRelatedField(
queryset=model.objects.all(), queryset=model.objects.all(),
@ -45,7 +69,7 @@ class BarcodeAssignMixin(serializers.Serializer):
"""Return a list of model fields.""" """Return a list of model fields."""
fields = [ fields = [
model.barcode_model_type() model.barcode_model_type()
for model in InvenTreeInternalBarcodePlugin.get_supported_barcode_models() for model in plugin.base.barcodes.helper.get_supported_barcode_models()
] ]
return fields return fields

View File

@ -9,28 +9,44 @@ references model objects actually exist in the database.
import json import json
from django.core.validators import MaxLengthValidator, MinLengthValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import plugin.base.barcodes.helper
from InvenTree.helpers import hash_barcode from InvenTree.helpers import hash_barcode
from InvenTree.helpers_model import getModelsWithMixin
from InvenTree.models import InvenTreeBarcodeMixin from InvenTree.models import InvenTreeBarcodeMixin
from plugin import InvenTreePlugin from plugin import InvenTreePlugin
from plugin.mixins import BarcodeMixin from plugin.mixins import BarcodeMixin, SettingsMixin
class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin): class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugin):
"""Builtin BarcodePlugin for matching and generating internal barcodes.""" """Builtin BarcodePlugin for matching and generating internal barcodes."""
NAME = 'InvenTreeBarcode' NAME = 'InvenTreeBarcode'
TITLE = _('InvenTree Barcodes') TITLE = _('InvenTree Barcodes')
DESCRIPTION = _('Provides native support for barcodes') DESCRIPTION = _('Provides native support for barcodes')
VERSION = '2.0.0' VERSION = '2.1.0'
AUTHOR = _('InvenTree contributors') AUTHOR = _('InvenTree contributors')
@staticmethod SETTINGS = {
def get_supported_barcode_models(): 'INTERNAL_BARCODE_FORMAT': {
"""Returns a list of database models which support barcode functionality.""" 'name': _('Internal Barcode Format'),
return getModelsWithMixin(InvenTreeBarcodeMixin) 'description': _('Select an internal barcode format'),
'choices': [
('json', _('JSON barcodes (require more space)')),
('short', _('Short barcodes (made for optimized space)')),
],
'default': 'json',
},
'SHORT_BARCODE_PREFIX': {
'name': _('Short Barcode Prefix'),
'description': _(
'Customize the prefix used for short barcodes, may be useful for environments with multiple InvenTree instances'
),
'validator': [str, MinLengthValidator(4), MaxLengthValidator(4)],
'default': 'INV-',
},
}
def format_matched_response(self, label, model, instance): def format_matched_response(self, label, model, instance):
"""Format a response for the scanned data.""" """Format a response for the scanned data."""
@ -53,7 +69,7 @@ class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
supported_models = self.get_supported_barcode_models() supported_models = plugin.base.barcodes.helper.get_supported_barcode_models()
if barcode_dict is not None and type(barcode_dict) is dict: if barcode_dict is not None and type(barcode_dict) is dict:
# Look for various matches. First good match will be returned # Look for various matches. First good match will be returned
@ -79,3 +95,18 @@ class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
if instance is not None: if instance is not None:
return self.format_matched_response(label, model, instance) return self.format_matched_response(label, model, instance)
def generate(self, model_instance: InvenTreeBarcodeMixin):
"""Generate a barcode for a given model instance."""
barcode_format = self.get_setting('INTERNAL_BARCODE_FORMAT')
if barcode_format == 'json':
return json.dumps({model_instance.barcode_model_type(): model_instance.pk})
if barcode_format == 'short':
prefix = self.get_setting('SHORT_BARCODE_PREFIX')
model_type_code = model_instance.barcode_model_type_code()
return f'{prefix}{model_type_code}{model_instance.pk}'
return None

View File

@ -142,11 +142,16 @@ class StockLocation(
"""Return API url.""" """Return API url."""
return reverse('api-location-list') return reverse('api-location-list')
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'SL'
def report_context(self): def report_context(self):
"""Return report context data for this StockLocation.""" """Return report context data for this StockLocation."""
return { return {
'location': self, 'location': self,
'qr_data': self.format_barcode(brief=True), 'qr_data': self.barcode,
'parent': self.parent, 'parent': self.parent,
'stock_location': self, 'stock_location': self,
'stock_items': self.get_stock_items(), 'stock_items': self.get_stock_items(),
@ -367,6 +372,11 @@ class StockItem(
"""Custom API instance filters.""" """Custom API instance filters."""
return {'parent': {'exclude_tree': self.pk}} return {'parent': {'exclude_tree': self.pk}}
@classmethod
def barcode_model_type_code(cls):
"""Return the associated barcode model type code for this model."""
return 'SI'
def get_test_keys(self, include_installed=True): def get_test_keys(self, include_installed=True):
"""Construct a flattened list of test 'keys' for this StockItem.""" """Construct a flattened list of test 'keys' for this StockItem."""
keys = [] keys = []
@ -397,7 +407,7 @@ class StockItem(
'item': self, 'item': self,
'name': self.part.full_name, 'name': self.part.full_name,
'part': self.part, 'part': self.part,
'qr_data': self.format_barcode(brief=True), 'qr_data': self.barcode,
'qr_url': self.get_absolute_url(), 'qr_url': self.get_absolute_url(),
'parameters': self.part.parameters_map(), 'parameters': self.part.parameters_map(),
'quantity': InvenTree.helpers.normalize(self.quantity), 'quantity': InvenTree.helpers.normalize(self.quantity),

View File

@ -16,6 +16,7 @@
{% include "InvenTree/settings/setting.html" with key="BARCODE_INPUT_DELAY" icon="fa-hourglass-half" %} {% include "InvenTree/settings/setting.html" with key="BARCODE_INPUT_DELAY" icon="fa-hourglass-half" %}
{% include "InvenTree/settings/setting.html" with key="BARCODE_WEBCAM_SUPPORT" icon="fa-video" %} {% include "InvenTree/settings/setting.html" with key="BARCODE_WEBCAM_SUPPORT" icon="fa-video" %}
{% include "InvenTree/settings/setting.html" with key="BARCODE_SHOW_TEXT" icon="fa-closed-captioning" %} {% include "InvenTree/settings/setting.html" with key="BARCODE_SHOW_TEXT" icon="fa-closed-captioning" %}
{% include "InvenTree/settings/setting.html" with key="BARCODE_GENERATION_PLUGIN" icon="fa-qrcode" %}
</tbody> </tbody>
</table> </table>

View File

@ -98,7 +98,8 @@ export default function SystemSettings() {
'BARCODE_ENABLE', 'BARCODE_ENABLE',
'BARCODE_INPUT_DELAY', 'BARCODE_INPUT_DELAY',
'BARCODE_WEBCAM_SUPPORT', 'BARCODE_WEBCAM_SUPPORT',
'BARCODE_SHOW_TEXT' 'BARCODE_SHOW_TEXT',
'BARCODE_GENERATION_PLUGIN'
]} ]}
/> />
) )