From 0ffd920f0878a918f19e807dbb82535932ebc0c8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 25 Oct 2022 23:04:09 +1100 Subject: [PATCH 1/9] Adds mixin class for assigning and un-assigning barcodes --- inventree/base.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/inventree/base.py b/inventree/base.py index f41ad16..ddbf051 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -562,3 +562,57 @@ def complete(self, **kwargs): def cancel(self, **kwargs): return self._statusupdate(status='cancel', **kwargs) + + +class BarcodeMixin: + """Adds barcode scanning functionality to various data types. + + Any class which inherits from this mixin can assign (or un-assign) barcode data. + """ + + @classmethod + def barcodeModelType(cls): + """Return the model type name required for barcode assignment. + + Default value is the lower-case class name () + """ + return cls.__name__.lower() + + def assignBarcode(self, barcode_data: str, reload=True): + """Assign an arbitrary barcode to this object (in the database). + + Arguments: + barcode_data: A string containing arbitrary barcode data + """ + + model_type = self.barcodeModelType() + + response = self._api.post( + '/barcode/link/', + { + 'barcode': barcode_data, + model_type: self.pk, + } + ) + + if reload: + self.reload() + + return response + + def unassignBarcode(self, reload=True): + """Unassign a barcode from this object""" + + model_type = self.barcodeModelType() + + response = self._api.post( + '/barcode/unlink/', + { + model_type: self.pk, + } + ) + + if reload: + self.reload() + + return response \ No newline at end of file From 9cbdf2257f58aa9c3b190ad0c6be251b99b4d305 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 25 Oct 2022 23:04:26 +1100 Subject: [PATCH 2/9] Some basic unit tests for assigning / unassigning barcodes from part object --- inventree/part.py | 2 +- test/test_part.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/inventree/part.py b/inventree/part.py index b9303c3..c3e86c3 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -58,7 +58,7 @@ def getCategoryParameterTemplates(self, fetch_parent: bool = True) -> list: ) -class Part(inventree.base.MetadataMixin, inventree.base.ImageMixin, inventree.label.LabelPrintingMixing, inventree.base.InventreeObject): +class Part(inventree.base.BarcodeMixin, inventree.base.MetadataMixin, inventree.base.ImageMixin, inventree.label.LabelPrintingMixing, inventree.base.InventreeObject): """ Class representing the Part database model """ URL = 'part' diff --git a/test/test_part.py b/test/test_part.py index ada3b10..fd4356e 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -603,3 +603,36 @@ def test_metadata(self): self.assertEqual(len(metadata.keys()), 2) self.assertEqual(metadata['foo'], 'rab') self.assertEqual(metadata['hello'], 'world') + + +class PartBarcodeTest(InvenTreeTestCase): + """Tests for Part barcode functionality""" + + def test_barcode_assign(self): + """Tests for assigning barcodes to Part instances""" + + # Grab a part from the database + part_1 = Part(self.api, pk=1) + + # First ensure that there is *no* barcode assigned to this item + part_1.unassignBarcode() + + # Assign a barcode to this part (should auto-reload) + response = part_1.assignBarcode('hello world') + + self.assertEqual(response['success'], 'Assigned barcode to part instance') + self.assertEqual(response['barcode_data'], 'hello world') + + # Attempt to assign the same barcode to a different part (should error) + part_2 = Part(self.api, pk=2) + + with self.assertRaises(HTTPError): + response = part_2.assignBarcode('hello world') + + # Unassign from part_1 + part_1.unassignBarcode() + + # Now assign to part_2 + response = part_2.assignBarcode('hello world') + self.assertEqual(response['barcode_data'], 'hello world') + \ No newline at end of file From 38591fec8f259a382f0e4858babb6b04e01f5b5f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 25 Oct 2022 23:14:47 +1100 Subject: [PATCH 3/9] Adds scanBarcode method to API class - Extend unit testing --- inventree/api.py | 12 ++++++++++++ test/test_part.py | 34 ++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index 8558a04..8a360d6 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -555,3 +555,15 @@ def downloadFile(self, url, destination, overwrite=False): logger.info(f"Downloaded '{url}' to '{destination}'") return True + + def scanBarcode(self, barcode_data): + """Scan a barcode to see if it matches a known object""" + + response = self.post( + '/barcode/', + { + 'barcode': barcode_data, + } + ) + + return response diff --git a/test/test_part.py b/test/test_part.py index fd4356e..6c8d50d 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from email.policy import HTTP import os import requests import sys @@ -611,6 +612,8 @@ class PartBarcodeTest(InvenTreeTestCase): def test_barcode_assign(self): """Tests for assigning barcodes to Part instances""" + barcode = 'ABCD-1234-XYZ' + # Grab a part from the database part_1 = Part(self.api, pk=1) @@ -618,21 +621,40 @@ def test_barcode_assign(self): part_1.unassignBarcode() # Assign a barcode to this part (should auto-reload) - response = part_1.assignBarcode('hello world') + response = part_1.assignBarcode(barcode) self.assertEqual(response['success'], 'Assigned barcode to part instance') - self.assertEqual(response['barcode_data'], 'hello world') + self.assertEqual(response['barcode_data'], barcode) # Attempt to assign the same barcode to a different part (should error) part_2 = Part(self.api, pk=2) + # Ensure this part does not have an associated barcode + part_2.unassignBarcode() + with self.assertRaises(HTTPError): - response = part_2.assignBarcode('hello world') + response = part_2.assignBarcode(barcode) + + # Scan the barcode (should point back to part_1) + response = self.api.scanBarcode(barcode) + + self.assertEqual(response['barcode_data'], barcode) + self.assertEqual(response['part']['pk'], 1) # Unassign from part_1 part_1.unassignBarcode() # Now assign to part_2 - response = part_2.assignBarcode('hello world') - self.assertEqual(response['barcode_data'], 'hello world') - \ No newline at end of file + response = part_2.assignBarcode(barcode) + self.assertEqual(response['barcode_data'], barcode) + + # Scan again + response = self.api.scanBarcode(barcode) + self.assertEqual(response['part']['pk'], 2) + + # Unassign from part_2 + part_2.unassignBarcode() + + # Scanning this time should yield no results + with self.assertRaises(HTTPError): + response = self.api.scanBarcode(barcode) From a7173f4a262d894148cb383fc5cc92733814d843 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 25 Oct 2022 23:15:50 +1100 Subject: [PATCH 4/9] Remove weird import --- test/test_part.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_part.py b/test/test_part.py index 6c8d50d..8987ddf 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from email.policy import HTTP import os import requests import sys From e5ecd28c7aa0583ca4de517a1d12e65f57dd1fef Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 25 Oct 2022 23:19:54 +1100 Subject: [PATCH 5/9] Add mixin class to other supported model types --- inventree/company.py | 2 +- inventree/stock.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/inventree/company.py b/inventree/company.py index 5b4beb9..bd376f6 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -63,7 +63,7 @@ def createSalesOrder(self, **kwargs): ) -class SupplierPart(inventree.base.BulkDeleteMixin, inventree.base.InventreeObject): +class SupplierPart(inventree.base.BarcodeMixin, inventree.base.BulkDeleteMixin, inventree.base.InventreeObject): """Class representing the SupplierPart database model - Implements the BulkDeleteMixin diff --git a/inventree/stock.py b/inventree/stock.py index 3f12a24..4db7e55 100644 --- a/inventree/stock.py +++ b/inventree/stock.py @@ -9,7 +9,7 @@ import inventree.label -class StockLocation(inventree.base.MetadataMixin, inventree.label.LabelPrintingMixing, inventree.base.InventreeObject): +class StockLocation(inventree.base.BarcodeMixin, inventree.base.MetadataMixin, inventree.label.LabelPrintingMixing, inventree.base.InventreeObject): """ Class representing the StockLocation database model """ URL = 'stock/location' @@ -39,7 +39,7 @@ def getChildLocations(self, **kwargs): return StockLocation.list(self._api, parent=self.pk, **kwargs) -class StockItem(inventree.base.BulkDeleteMixin, inventree.base.MetadataMixin, inventree.label.LabelPrintingMixing, inventree.base.InventreeObject): +class StockItem(inventree.base.BarcodeMixin, inventree.base.BulkDeleteMixin, inventree.base.MetadataMixin, inventree.label.LabelPrintingMixing, inventree.base.InventreeObject): """Class representing the StockItem database model.""" URL = 'stock' From 2b42fcbda8f4c4840f7e5b784b38cdd4fea9f199 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Oct 2022 18:43:49 +1100 Subject: [PATCH 6/9] Unit tests for StockItem barcode functionality --- test/test_stock.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/test_stock.py b/test/test_stock.py index 82fd1cf..9f9cc59 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -219,6 +219,36 @@ def test_bulk_delete(self): items = loc.getStockItems() self.assertEqual(len(items), 0) + def test_barcode_support(self): + """Test barcode support for the StockItem model""" + + items = StockItem.list(self.api, limit=10) + + for item in items: + # Delete any existing barcode + item.unassignBarcode() + + # Perform lookup based on 'internal' barcode + response = self.api.scanBarcode( + { + 'stockitem': item.pk, + } + ) + + self.assertEqual(response['stockitem']['pk'], item.pk) + self.assertEqual(response['plugin'], 'InvenTreeInternalBarcode') + + # Assign a custom barcode to this StockItem + barcode = f"custom-stock-item-{item.pk}" + item.assignBarcode(barcode) + + response = self.api.scanBarcode(barcode) + + self.assertEqual(response['stockitem']['pk'], item.pk) + self.assertEqual(response['plugin'], 'InvenTreeExternalBarcode') + self.assertEqual(response['barcode_data'], barcode) + + item.unassignBarcode() class StockAdjustTest(InvenTreeTestCase): """Unit tests for stock 'adjustment' actions""" From ceb6f65fd135edd636cf3104896584ed98d91d04 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Oct 2022 18:45:42 +1100 Subject: [PATCH 7/9] Pipeline fixes --- .github/workflows/ci.yaml | 1 + test/test_stock.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1e8918e..7b925b7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,6 +12,7 @@ jobs: INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3 INVENTREE_MEDIA_ROOT: ../test_inventree_media INVENTREE_STATIC_ROOT: ../test_inventree_static + INVENTREE_BACKUP_DIR: ../test_inventree_backup INVENTREE_ADMIN_USER: testuser INVENTREE_ADMIN_PASSWORD: testpassword INVENTREE_ADMIN_EMAIL: test@test.com diff --git a/test/test_stock.py b/test/test_stock.py index 9f9cc59..cc8d815 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -250,6 +250,7 @@ def test_barcode_support(self): item.unassignBarcode() + class StockAdjustTest(InvenTreeTestCase): """Unit tests for stock 'adjustment' actions""" From 2bf176ec1ec246485c19bd8d053a51d94044ad69 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Oct 2022 18:46:27 +1100 Subject: [PATCH 8/9] PEP style fixes --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index ddbf051..74a3367 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -615,4 +615,4 @@ def unassignBarcode(self, reload=True): if reload: self.reload() - return response \ No newline at end of file + return response From 753668a2d4ebd9c0238ae703a24afbbe705472a1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Oct 2022 18:50:45 +1100 Subject: [PATCH 9/9] Use python 3.9 for CI tests --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7b925b7..c1ba419 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,7 +23,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.8] + python-version: [3.9] steps: - name: Checkout Code