Skip to content

Commit

Permalink
Merge pull request #144 from inventree/barcode-actions
Browse files Browse the repository at this point in the history
Barcode actions
  • Loading branch information
SchrodingersGat authored Oct 26, 2022
2 parents 3f226c8 + 753668a commit b5527c5
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: [email protected]
Expand All @@ -22,7 +23,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.8]
python-version: [3.9]

steps:
- name: Checkout Code
Expand Down
12 changes: 12 additions & 0 deletions inventree/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 54 additions & 0 deletions inventree/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion inventree/company.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion inventree/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions inventree/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
54 changes: 54 additions & 0 deletions test/test_part.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,3 +603,57 @@ 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"""

barcode = 'ABCD-1234-XYZ'

# 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(barcode)

self.assertEqual(response['success'], 'Assigned barcode to part instance')
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(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(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)
31 changes: 31 additions & 0 deletions test/test_stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,37 @@ 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"""
Expand Down

0 comments on commit b5527c5

Please sign in to comment.