Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add barcode generation capabilities to plugins #7648

Merged
merged 35 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
24bd5d0
initial implementation of barcode generation using plugins
wolflu05 Jul 14, 2024
e80b00f
implement short QR code scanning
wolflu05 Jul 14, 2024
2162eef
add PUI qrcode preview
wolflu05 Jul 14, 2024
c6b665b
use barcode generation for CUI show barcode modal
wolflu05 Jul 14, 2024
3999ae6
remove short qr prefix validators and fix short qr detection regex
wolflu05 Jul 14, 2024
650787a
catch errors if model with pk is not found for scanning and generating
wolflu05 Jul 15, 2024
cb105a6
improve qrcode templatetag
wolflu05 Jul 15, 2024
508258b
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 15, 2024
ba925ee
fix comments
wolflu05 Jul 15, 2024
832130f
fix for python 3.9
wolflu05 Jul 15, 2024
05c8db8
add tests
wolflu05 Jul 15, 2024
2d0e194
fix: tests
wolflu05 Jul 15, 2024
f2c76dc
add docs
wolflu05 Jul 15, 2024
09525f4
fix: tests
wolflu05 Jul 15, 2024
e63cfe5
bump api version
wolflu05 Jul 15, 2024
8a68d8b
add docs to BarcodeMixin
wolflu05 Jul 15, 2024
f24cbab
fix: test
wolflu05 Jul 15, 2024
d5ab4c8
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 16, 2024
e86bddc
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 16, 2024
e794b2b
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 17, 2024
6dd2c64
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 17, 2024
a6b3f4a
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 18, 2024
1e9ac3c
added suggestions from code review
wolflu05 Jul 19, 2024
aebf7ab
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 19, 2024
3a2d46f
fix: tests
wolflu05 Jul 19, 2024
76043ed
Add MinLengthValidator to short barcode prefix setting
wolflu05 Jul 19, 2024
9a6143f
fix: tests?
wolflu05 Jul 21, 2024
3f532e0
Merge branch 'barcode-generation' of github.com:wolflu05/InvenTree in…
wolflu05 Jul 21, 2024
6674f03
trigger: ci
wolflu05 Jul 21, 2024
a486aa6
Merge remote-tracking branch 'upstream/master' into barcode-generation
wolflu05 Jul 21, 2024
dced23e
try custom cache
wolflu05 Jul 21, 2024
2a98460
try custom cache ignore all falsy
wolflu05 Jul 21, 2024
e999619
remove debugging
wolflu05 Jul 21, 2024
60141b8
Revert "Add MinLengthValidator to short barcode prefix setting"
wolflu05 Jul 21, 2024
3475637
Revert "fix: tests"
wolflu05 Jul 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion docs/docs/barcodes/internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,61 @@ title: Internal Barcodes

## Internal Barcodes

InvenTree defines an internal format for generating barcodes for various items. This format uses a simple JSON-style string to uniquely identify an item in the database.
InvenTree ships with two integrated internal formats for generating barcodes for various items which are available through the built-in InvenTree Barcode plugin. The used format can be selected through the plugin settings of the InvenTree Barcode plugin.

### 1. JSON-based QR Codes

This format uses a simple JSON-style string to uniquely identify an item in the database.

Some simple examples of this format are shown below:

| Model Type | Example Barcode |
| --- | --- |
| Part | `{% raw %}{"part": 10}{% endraw %}` |
| Stock Item | `{% raw %}{"stockitem": 123}{% endraw %}` |
| Stock Location | `{% raw %}{"stocklocation": 1}{% endraw %}` |
| Supplier Part | `{% raw %}{"supplierpart": 99}{% endraw %}` |

The numerical ID value used is the *Primary Key* (PK) of the particular object in the database.

#### Downsides

1. The JSON format includes binary only characters (`{% raw %}{{% endraw %}` and `{% raw %}"{% endraw %}`) which requires unnecessary use of the binary QR code encoding which means fewer amount of chars can be encoded with the same version of QR code.
2. The model name key has not a fixed length. Some model names are longer than others. E.g. a part QR code with the shortest possible id requires 11 chars, while a stock location QR code with the same id would already require 20 chars, which already requires QR code version 2 and quickly version 3.

!!! info "QR code versions"
There are 40 different qr code versions from 1-40. They all can encode more data than the previous version, but require more "squares". E.g. a V1 QR codes has 21x21 "squares" while a V2 already has 25x25. For more information see [QR code comparison](https://www.qrcode.com/en/about/version.html).

For a more detailed size analysis of the JSON-based QR codes refer to [this issue](https://github.com/inventree/InvenTree/issues/6612).

### 2. Short alphanumeric QR Codes

While JSON-based QR Codes encode all necessary information, they come with the described downsides. This new, short, alphanumeric only format is build to improve those downsides. The basic format uses an alphanumeric string: `INV-??x`

- `INV-` is a constant prefix. This is configurable in the InvenTree Barcode plugins settings per instance to support environments that use multiple instances.
- `??` is a two character alphanumeric (`0-9A-Z $%*+-./:` (45 chars)) code, individual to each model.
- `x` the actual pk of the model.

Now with an overhead of 6 chars for every model, this format supports the following amount of model instances using the described QR code modes:

| QR code mode | Alphanumeric mode | Mixed mode |
| --- | --- | --- |
| v1 M ECL (15%) | `10**14` items (~3.170 items per sec for 1000 years) | `10**20` items (~3.170.979.198 items per sec for 1000 years) |
| v1 Q ECL (25%) | `10**10` items (~0.317 items per sec for 1000 years) | `10**13` items (~317 items per sec for 1000 years) |
| v1 H ECL (30%) | `10**4` items (~100 items per day for 100 days) | `10**3` items (~100 items per day for 10 days (*even worse*)) |

!!! info "QR code mixed mode"
Normally the QR code data is encoded only in one format (binary, alphanumeric, numeric). But the data can also be split into multiple chunks using different formats. This is especially useful with long model ids, because the first 6 chars can be encoded using the alphanumeric mode and the id using the more efficient numeric mode. Mixed mode is used by default, because the `qrcode` template tag uses a default value for optimize of 1.

Some simple examples of this format are shown below:

| Model Type | Example Barcode |
| --- | --- |
| Part | `INV-PA10` |
| Stock Item | `INV-SI123` |
| Stock Location | `INV-SL1` |
| Supplier Part | `INV-SP99` |

## Report Integration

This barcode format can be used to generate 1D or 2D barcodes (e.g. for [labels and reports](../report/barcodes.md))
Expand Down
49 changes: 35 additions & 14 deletions docs/docs/extend/plugins/barcode.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Barcode Mixin

## Barcode Plugins

InvenTree supports decoding of arbitrary barcode data via a **Barcode Plugin** interface. Barcode data POSTed to the `/api/barcode/` endpoint will be supplied to all loaded barcode plugins, and the first plugin to successfully interpret the barcode data will return a response to the client.
InvenTree supports decoding of arbitrary barcode data and generation of internal barcode formats via a **Barcode Plugin** interface. Barcode data POSTed to the `/api/barcode/` endpoint will be supplied to all loaded barcode plugins, and the first plugin to successfully interpret the barcode data will return a response to the client.

InvenTree can generate native QR codes to represent database objects (e.g. a single StockItem). This barcode can then be used to perform quick lookup of a stock item or location in the database. A client application (for example the InvenTree mobile app) scans a barcode, and sends the barcode data to the InvenTree server. The server then uses the **InvenTreeBarcodePlugin** (found at `src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py`) to decode the supplied barcode data.

Expand All @@ -26,7 +26,7 @@ POST {

### Builtin Plugin

The InvenTree server includes a builtin barcode plugin which can decode QR codes generated by the server. This plugin is enabled by default.
The InvenTree server includes a builtin barcode plugin which can generate and decode the QR codes. This plugin is enabled by default.

::: plugin.builtin.barcodes.inventree_barcode.InvenTreeInternalBarcodePlugin
options:
Expand All @@ -39,14 +39,12 @@ The InvenTree server includes a builtin barcode plugin which can decode QR codes

### Example Plugin

Please find below a very simple example that is executed each time a barcode is scanned.
Please find below a very simple example that is used to return a part if the barcode starts with `PART-`

```python
from django.utils.translation import gettext_lazy as _

from InvenTree.models import InvenTreeBarcodeMixin
from plugin import InvenTreePlugin
from plugin.mixins import BarcodeMixin
from part.models import Part

class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):

Expand All @@ -56,16 +54,39 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
VERSION = "0.0.1"
AUTHOR = "Michael"

status = 0

def scan(self, barcode_data):
if barcode_data.startswith("PART-"):
try:
pk = int(barcode_data.split("PART-")[1])
instance = Part.objects.get(pk=pk)
label = Part.barcode_model_type()

return {label: instance.format_matched_response()}
except Part.DoesNotExist:
pass
```

To try it just copy the file to src/InvenTree/plugins and restart the server. Open the scan barcode window and start to scan codes or type in text manually. Each time the timeout is hit the plugin will execute and printout the result. The timeout can be changed in `Settings->Barcode Support->Barcode Input Delay`.

### Custom Internal Format

To implement a custom internal barcode format, the `generate(...)` method from the Barcode Mixin needs to be overridden. Then the plugin can be selected at `System Settings > Barcodes > Barcode Generation Plugin`.

self.status = self.status+1
print('Started barcode plugin', self.status)
print(barcode_data)
response = {}
return response
```python
from InvenTree.models import InvenTreeBarcodeMixin
from plugin import InvenTreePlugin
from plugin.mixins import BarcodeMixin

class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
NAME = "MyInternalBarcode"
TITLE = "My Internal Barcodes"
DESCRIPTION = "support for custom internal barcodes"
VERSION = "0.0.1"
AUTHOR = "InvenTree contributors"

def generate(self, model_instance: InvenTreeBarcodeMixin):
return f'{model_instance.barcode_model_type()}: {model_instance.pk}'
```

To try it just copy the file to src/InvenTree/plugins and restart the server. Open the scan barcode window and start to scan codes or type in text manually. Each time the timeout is hit the plugin will execute and printout the result. The timeout can be changed in `Settings->Barcode Support->Barcode Input Delay`.
!!! info "Scanning implementation required"
The parsing of the custom format needs to be implemented too, so that the scanning of the generated QR codes resolves to the correct part.
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 225
INVENTREE_API_VERSION = 226

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
- Adds barcode generation API endpoint

v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
- Adds "filters" field to DataImportSession API

Expand Down
32 changes: 0 additions & 32 deletions src/backend/InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,38 +396,6 @@ def WrapWithQuotes(text, quote='"'):
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():
"""Return a list of allowable file formats for importing or exporting tabular data."""
return ['csv', 'xlsx', 'tsv', 'json']
Expand Down
7 changes: 1 addition & 6 deletions src/backend/InvenTree/InvenTree/helpers_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
from djmoney.money import Money
from PIL import Image

import InvenTree
import InvenTree.helpers_model
import InvenTree.version
from common.notifications import (
InvenTreeNotificationBodies,
NotificationBody,
Expand Down Expand Up @@ -331,9 +328,7 @@ def notify_users(
'instance': instance,
'name': content.name.format(**content_context),
'message': content.message.format(**content_context),
'link': InvenTree.helpers_model.construct_absolute_url(
instance.get_absolute_url()
),
'link': construct_absolute_url(instance.get_absolute_url()),
'template': {'subject': content.name.format(**content_context)},
}

Expand Down
26 changes: 20 additions & 6 deletions src/backend/InvenTree/InvenTree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import logging
from datetime import datetime

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
Expand Down Expand Up @@ -934,6 +932,8 @@ class InvenTreeBarcodeMixin(models.Model):

- barcode_data : Raw data associated with an assigned barcode
- 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:
Expand Down Expand Up @@ -964,11 +964,25 @@ def barcode_model_type(cls):
# By default, use the name of the class
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):
"""Return a JSON string for formatting a QR code for this model instance."""
return InvenTree.helpers.MakeBarcode(
self.__class__.barcode_model_type(), self.pk, **kwargs
)
from plugin.base.barcodes.helper import generate_barcode

return generate_barcode(self)

def format_matched_response(self):
"""Format a standard response for a matched barcode."""
Expand All @@ -986,7 +1000,7 @@ def format_matched_response(self):
@property
def barcode(self):
"""Format a minimal barcode string (e.g. for label printing)."""
return self.format_barcode(brief=True)
return self.format_barcode()

@classmethod
def lookup_barcode(cls, barcode_hash):
Expand Down
27 changes: 0 additions & 27 deletions src/backend/InvenTree/InvenTree/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,33 +789,6 @@ def tests(self):
self.assertEqual(result, b)


class TestMakeBarcode(TestCase):
"""Tests for barcode string creation."""

def test_barcode_extended(self):
"""Test creation of barcode with extended data."""
bc = helpers.MakeBarcode(
'part', 3, {'id': 3, 'url': 'www.google.com'}, brief=False
)

self.assertIn('part', bc)
self.assertIn('tool', bc)
self.assertIn('"tool": "InvenTree"', bc)

data = json.loads(bc)

self.assertEqual(data['part']['id'], 3)
self.assertEqual(data['part']['url'], 'www.google.com')

def test_barcode_brief(self):
"""Test creation of simple barcode."""
bc = helpers.MakeBarcode('stockitem', 7)

data = json.loads(bc)
self.assertEqual(len(data), 1)
self.assertEqual(data['stockitem'], 7)


class TestDownloadFile(TestCase):
"""Tests for DownloadFile."""

Expand Down
5 changes: 5 additions & 0 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def api_defaults(cls, request=None):

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):
"""Custom save method for the BuildOrder model"""
self.validate_reference_field(self.reference)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ <h3>
$('#show-qr-code').click(function() {
showQRDialog(
'{% trans "Build Order QR Code" escape %}',
'{"build": {{ build.pk }} }'
'{{ build.barcode }}'
);
});

Expand Down
23 changes: 21 additions & 2 deletions src/backend/InvenTree/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import json
import logging
import os
import sys
import uuid
from datetime import timedelta, timezone
from enum import Enum
from io import BytesIO
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.conf import settings as django_settings
Expand Down Expand Up @@ -49,13 +50,25 @@
import InvenTree.tasks
import InvenTree.validators
import order.validators
import plugin.base.barcodes.helper
import report.helpers
import users.models
from InvenTree.sanitizer import sanitize_svg
from plugin import registry

logger = logging.getLogger('inventree')

if sys.version_info >= (3, 11):
from typing import NotRequired
else:

class NotRequired: # pragma: no cover
"""NotRequired type helper is only supported with Python 3.11+."""

def __class_getitem__(cls, item):
"""Return the item."""
return item


class MetaMixin(models.Model):
"""A base class for InvenTree models to include shared meta fields.
Expand Down Expand Up @@ -1167,7 +1180,7 @@ class InvenTreeSettingsKeyType(SettingsKeyType):
requires_restart: If True, a server restart is required after changing the setting
"""

requires_restart: bool
requires_restart: NotRequired[bool]


class InvenTreeSetting(BaseInvenTreeSetting):
Expand Down Expand Up @@ -1402,6 +1415,12 @@ def save(self, *args, **kwargs):
'default': False,
'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': {
'name': _('Part Revisions'),
'description': _('Enable revision field for Part'),
Expand Down
Loading
Loading