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:
@ -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
|
||||
|
23
InvenTree/stock/migrations/0084_auto_20220903_0154.py
Normal file
23
InvenTree/stock/migrations/0084_auto_20220903_0154.py
Normal 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'),
|
||||
),
|
||||
]
|
48
InvenTree/stock/migrations/0085_auto_20220903_0225.py
Normal file
48
InvenTree/stock/migrations/0085_auto_20220903_0225.py
Normal 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
|
||||
)
|
||||
]
|
17
InvenTree/stock/migrations/0086_remove_stockitem_uid.py
Normal file
17
InvenTree/stock/migrations/0086_remove_stockitem_uid.py
Normal 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',
|
||||
),
|
||||
]
|
23
InvenTree/stock/migrations/0087_auto_20220912_2341.py
Normal file
23
InvenTree/stock/migrations/0087_auto_20220912_2341.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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() {
|
||||
|
@ -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 () {
|
||||
|
@ -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):
|
||||
|
Reference in New Issue
Block a user