2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

Barcode Refactor (#3640)

* define a simple model mixin class for barcode

* Adds generic function for assigning a barcode to a model instance

* StockItem model now implements the BarcodeMixin class

* Implement simple unit tests for new code

* Fix unit tests

* Data migration for uid field

* Remove references to old 'uid' field

* Migration for removing old uid field from StockItem model

* Bump API version

* Change lookup_barcode to be a classmethod

* Change barcode_model_type to be a class method

* Cleanup for generic barcode scan and assign API:

- Raise ValidationError as appropriate
- Improved unit testing
- Groundwork for future generic implementation

* Further unit tests for barcode scanning

* Adjust error messages for compatibility

* Unit test fix

* Fix hash_barcode function

- Add unit tests to ensure it produces the same results as before the refactor

* Add BarcodeMixin to Part model

* Remove old format_barcode function from Part model

* Further fixes for unit tests

* Add support for assigning arbitrary barcode to Part instance

- Simplify barcode API
- Add more unit tests

* More unit test fixes

* Update unit test

* Adds generic endpoint for unassigning barcode data

* Update web dialog for unlinking a barcode

* Template cleanup

* Add Barcode mixin to StockLocation class

* Add some simple unit tests for new model mixin

* Support assigning / unassigning barcodes for StockLocation

* remove failing outdated test

* Update template to integrate new barcode support for StockLocation

* Add BarcodeMixin to SupplierPart model

* Adds QR code view for SupplierPart

* Major simplification of barcode API endpoints

- Separate existing barcode plugin into two separate classes
- Simplify and consolidate the response from barcode scanning
- Update unit testing

* Yet more unit test fixes

* Yet yet more unit test fixes
This commit is contained in:
Oliver
2022-09-15 14:14:51 +10:00
committed by GitHub
parent 7645492cc2
commit 187707c892
34 changed files with 1115 additions and 495 deletions

View File

@ -222,7 +222,7 @@
lft: 0
rght: 0
expiry_date: "1990-10-10"
uid: 9e5ae7fc20568ed4814c10967bba8b65
barcode_hash: 9e5ae7fc20568ed4814c10967bba8b65
- model: stock.stockitem
pk: 521
@ -236,7 +236,7 @@
lft: 0
rght: 0
status: 60
uid: 1be0dfa925825c5c6c79301449e50c2d
barcode_hash: 1be0dfa925825c5c6c79301449e50c2d
- model: stock.stockitem
pk: 522

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.15 on 2022-09-03 01:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0083_stocklocation_icon'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='barcode_data',
field=models.CharField(blank=True, help_text='Third party barcode data', max_length=500, verbose_name='Barcode Data'),
),
migrations.AddField(
model_name='stockitem',
name='barcode_hash',
field=models.CharField(blank=True, help_text='Unique hash of barcode data', max_length=128, verbose_name='Barcode Hash'),
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 3.2.15 on 2022-09-03 02:25
from django.db import migrations
def uid_to_barcode(apps, schama_editor):
"""Migrate old 'uid' field to new 'barcode_hash' field"""
StockItem = apps.get_model('stock', 'stockitem')
# Find all StockItem objects with non-empty UID field
items = StockItem.objects.exclude(uid=None).exclude(uid='')
for item in items:
item.barcode_hash = item.uid
item.save()
if items.count() > 0:
print(f"Updated barcode data for {items.count()} StockItem objects")
def barcode_to_uid(apps, schema_editor):
"""Migrate new 'barcode_hash' field to old 'uid' field"""
StockItem = apps.get_model('stock', 'stockitem')
# Find all StockItem objects with non-empty UID field
items = StockItem.objects.exclude(barcode_hash=None).exclude(barcode_hash='')
for item in items:
item.uid = item.barcode_hash
item.save()
if items.count() > 0:
print(f"Updated barcode data for {items.count()} StockItem objects")
class Migration(migrations.Migration):
dependencies = [
('stock', '0084_auto_20220903_0154'),
]
operations = [
migrations.RunPython(
uid_to_barcode,
reverse_code=barcode_to_uid
)
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.15 on 2022-09-03 02:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0085_auto_20220903_0225'),
]
operations = [
migrations.RemoveField(
model_name='stockitem',
name='uid',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.15 on 2022-09-12 23:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0086_remove_stockitem_uid'),
]
operations = [
migrations.AddField(
model_name='stocklocation',
name='barcode_data',
field=models.CharField(blank=True, help_text='Third party barcode data', max_length=500, verbose_name='Barcode Data'),
),
migrations.AddField(
model_name='stocklocation',
name='barcode_hash',
field=models.CharField(blank=True, help_text='Unique hash of barcode data', max_length=128, verbose_name='Barcode Hash'),
),
]

View File

@ -30,7 +30,8 @@ import report.models
from company import models as CompanyModels
from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeNotesField,
InvenTreeURLField)
from InvenTree.models import InvenTreeAttachment, InvenTreeTree, extract_int
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeTree, extract_int)
from InvenTree.status_codes import StockHistoryCode, StockStatus
from part import models as PartModels
from plugin.events import trigger_event
@ -38,7 +39,7 @@ from plugin.models import MetadataMixin
from users.models import Owner
class StockLocation(MetadataMixin, InvenTreeTree):
class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
"""Organization tree for StockItem objects.
A "StockLocation" can be considered a warehouse, or storage location
@ -126,27 +127,6 @@ class StockLocation(MetadataMixin, InvenTreeTree):
"""Return url for instance."""
return reverse('stock-location-detail', kwargs={'pk': self.id})
def format_barcode(self, **kwargs):
"""Return a JSON string for formatting a barcode for this StockLocation object."""
return InvenTree.helpers.MakeBarcode(
'stocklocation',
self.pk,
{
"name": self.name,
"url": reverse('api-location-detail', kwargs={'pk': self.id}),
},
**kwargs
)
@property
def barcode(self) -> str:
"""Get Brief payload data (e.g. for labels).
Returns:
str: Brief pyload data
"""
return self.format_barcode(brief=True)
def get_stock_items(self, cascade=True):
"""Return a queryset for all stock items under this category.
@ -221,12 +201,11 @@ def generate_batch_code():
return Template(batch_template).render(context)
class StockItem(MetadataMixin, MPTTModel):
class StockItem(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
"""A StockItem object represents a quantity of physical instances of a part.
Attributes:
parent: Link to another StockItem from which this StockItem was created
uid: Field containing a unique-id which is mapped to a third-party identifier (e.g. a barcode)
part: Link to the master abstract part that this StockItem is an instance of
supplier_part: Link to a specific SupplierPart (optional)
location: Where this StockItem is located
@ -552,38 +531,6 @@ class StockItem(MetadataMixin, MPTTModel):
"""Returns part name."""
return self.part.full_name
def format_barcode(self, **kwargs):
"""Return a JSON string for formatting a barcode for this StockItem.
Can be used to perform lookup of a stockitem using barcode.
Contains the following data:
`{ type: 'StockItem', stock_id: <pk>, part_id: <part_pk> }`
Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change)
"""
return InvenTree.helpers.MakeBarcode(
"stockitem",
self.id,
{
"request": kwargs.get('request', None),
"item_url": reverse('stock-item-detail', kwargs={'pk': self.id}),
"url": reverse('api-stock-detail', kwargs={'pk': self.id}),
},
**kwargs
)
@property
def barcode(self):
"""Get Brief payload data (e.g. for labels).
Returns:
str: Brief pyload data
"""
return self.format_barcode(brief=True)
uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
# Note: When a StockItem is deleted, a pre_delete signal handles the parent/child relationship
parent = TreeForeignKey(
'self',

View File

@ -62,7 +62,7 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
'quantity',
'serial',
'supplier_part',
'uid',
'barcode_hash',
]
def validate_serial(self, value):
@ -245,7 +245,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'supplier_part',
'supplier_part_detail',
'tracking_items',
'uid',
'barcode_hash',
'updated',
'purchase_price',
'purchase_price_currency',

View File

@ -44,7 +44,7 @@
<ul class='dropdown-menu' role='menu'>
<li><a class='dropdown-item' href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
{% if roles.stock.change %}
{% if item.uid %}
{% if item.barcode_hash %}
<li><a class='dropdown-item' href='#' id='barcode-unlink'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
{% else %}
<li><a class='dropdown-item' href='#' id='barcode-link'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>
@ -155,11 +155,11 @@
</td>
</tr>
{% if item.uid %}
{% if item.barcode_hash %}
<tr>
<td><span class='fas fa-barcode'></span></td>
<td>{% trans "Barcode Identifier" %}</td>
<td>{{ item.uid }}</td>
<td {% if item.barcode_data %}title='{{ item.barcode_data }}'{% endif %}>{{ item.barcode_hash }}</td>
</tr>
{% endif %}
{% if item.batch %}
@ -529,12 +529,22 @@ $("#show-qr-code").click(function() {
});
});
{% if barcodes %}
$("#barcode-link").click(function() {
linkBarcodeDialog({{ item.id }});
linkBarcodeDialog(
{
stockitem: {{ item.pk }},
},
{
title: '{% trans "Link Barcode to Stock Item" %}',
}
);
});
$("#barcode-unlink").click(function() {
unlinkBarcode({{ item.id }});
unlinkBarcode({
stockitem: {{ item.pk }},
});
});
$("#barcode-scan-into-location").click(function() {
@ -545,6 +555,7 @@ $("#barcode-scan-into-location").click(function() {
}
});
});
{% endif %}
{% if plugins_enabled %}
$('#locate-item-button').click(function() {

View File

@ -48,6 +48,11 @@
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
<ul class='dropdown-menu'>
<li><a class='dropdown-item' href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
{% if location.barcode_hash %}
<li><a class='dropdown-item' href='#' id='barcode-unlink'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
{% else %}
<li><a class='dropdown-item' href='#' id='barcode-link'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>
{% endif %}
{% if labels_enabled %}
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
{% endif %}
@ -135,6 +140,13 @@
</td>
</tr>
{% endif %}
{% if location and location.barcode_hash %}
<tr>
<td><span class='fas fa-barcode'></span></td>
<td>{% trans "Barcode Identifier" %}</td>
<td {% if location.barcode_data %}title='{{ location.barcode_data }}'{% endif %}>{{ location.barcode_hash }}</td>
</tr>
{% endif %}
</table>
{% endblock details_left %}
@ -335,6 +347,7 @@
adjustLocationStock('move');
});
{% if barcodes %}
$('#show-qr-code').click(function() {
launchModalForm("{% url 'stock-location-qr' location.id %}",
{
@ -342,6 +355,26 @@
});
});
$("#barcode-link").click(function() {
linkBarcodeDialog(
{
stocklocation: {{ location.pk }},
},
{
title: '{% trans "Link Barcode to Stock Location" %}',
}
);
});
$("#barcode-unlink").click(function() {
unlinkBarcode({
stocklocation: {{ location.pk }},
});
});
{% endif %}
{% endif %}
$('#item-create').click(function () {

View File

@ -14,8 +14,8 @@ from .models import (StockItem, StockItemTestResult, StockItemTracking,
StockLocation)
class StockTest(InvenTreeTestCase):
"""Tests to ensure that the stock location tree functions correcly."""
class StockTestBase(InvenTreeTestCase):
"""Base class for running Stock tests"""
fixtures = [
'category',
@ -44,6 +44,10 @@ class StockTest(InvenTreeTestCase):
Part.objects.rebuild()
StockItem.objects.rebuild()
class StockTest(StockTestBase):
"""Tests to ensure that the stock location tree functions correcly."""
def test_link(self):
"""Test the link URL field validation"""
@ -151,12 +155,6 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(self.home.get_absolute_url(), '/stock/location/1/')
def test_barcode(self):
"""Test format_barcode."""
barcode = self.office.format_barcode(brief=False)
self.assertIn('"name": "Office"', barcode)
def test_strings(self):
"""Test str function."""
it = StockItem.objects.get(pk=1)
@ -724,7 +722,38 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(C22.get_ancestors().count(), 1)
class VariantTest(StockTest):
class StockBarcodeTest(StockTestBase):
"""Run barcode tests for the stock app"""
def test_stock_item_barcode_basics(self):
"""Simple tests for the StockItem barcode integration"""
item = StockItem.objects.get(pk=1)
self.assertEqual(StockItem.barcode_model_type(), 'stockitem')
# Call format_barcode method
barcode = item.format_barcode(brief=False)
for key in ['tool', 'version', 'instance', 'stockitem']:
self.assertIn(key, barcode)
# Render simple barcode data for the StockItem
barcode = item.barcode
self.assertEqual(barcode, '{"stockitem": 1}')
def test_location_barcode_basics(self):
"""Simple tests for the StockLocation barcode integration"""
self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation')
loc = StockLocation.objects.get(pk=1)
barcode = loc.format_barcode(brief=True)
self.assertEqual('{"stocklocation": 1}', barcode)
class VariantTest(StockTestBase):
"""Tests for calculation stock counts against templates / variants."""
def test_variant_stock(self):
@ -805,7 +834,7 @@ class VariantTest(StockTest):
item.save()
class TestResultTest(StockTest):
class TestResultTest(StockTestBase):
"""Tests for the StockItemTestResult model."""
def test_test_count(self):