mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
initial implementation of barcode generation using plugins
This commit is contained in:
parent
0db280ad74
commit
24bd5d0962
@ -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']
|
||||||
|
@ -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)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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'),
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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')}"
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
48
src/backend/InvenTree/plugin/base/barcodes/helper.py
Normal file
48
src/backend/InvenTree/plugin/base/barcodes/helper.py
Normal 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()
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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'
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user