diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
index 70755eec47..22c65f2417 100644
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -2,11 +2,15 @@
 
 
 # InvenTree API version
-INVENTREE_API_VERSION = 75
+INVENTREE_API_VERSION = 76
 
 """
 Increment this API version number whenever there is a significant change to the API that any clients need to know about
 
+v76 -> 2022-09-10 : https://github.com/inventree/InvenTree/pull/3640
+    - Refactor of barcode data on the API
+    - StockItem.uid renamed to StockItem.barcode_hash
+
 v75 -> 2022-09-05 : https://github.com/inventree/InvenTree/pull/3644
     - Adds "pack_size" attribute to SupplierPart API serializer
 
diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index fcbe9f4c0e..52befbff8f 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -1,5 +1,6 @@
 """Provides helper functions used throughout the InvenTree project."""
 
+import hashlib
 import io
 import json
 import logging
@@ -907,6 +908,23 @@ def remove_non_printable_characters(value: str, remove_ascii=True, remove_unicod
     return cleaned
 
 
+def hash_barcode(barcode_data):
+    """Calculate a 'unique' hash for a barcode string.
+
+    This hash is used for comparison / lookup.
+
+    We first remove any non-printable characters from the barcode data,
+    as some browsers have issues scanning characters in.
+    """
+
+    barcode_data = str(barcode_data).strip()
+    barcode_data = remove_non_printable_characters(barcode_data)
+
+    hash = hashlib.md5(str(barcode_data).encode())
+
+    return str(hash.hexdigest())
+
+
 def get_objectreference(obj, type_ref: str = 'content_type', object_ref: str = 'object_id'):
     """Lookup method for the GenericForeignKey fields.
 
diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index 90741d51dd..61fab96f40 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -636,6 +636,103 @@ class InvenTreeTree(MPTTModel):
         return "{path} - {desc}".format(path=self.pathstring, desc=self.description)
 
 
+class InvenTreeBarcodeMixin(models.Model):
+    """A mixin class for adding barcode functionality to a model class.
+
+    Two types of barcodes are supported:
+
+    - Internal barcodes (QR codes using a strictly defined format)
+    - External barcodes (assign third party barcode data to a model instance)
+
+    The following fields are added to any model which implements this mixin:
+
+    - barcode_data : Raw data associated with an assigned barcode
+    - barcode_hash : A 'hash' of the assigned barcode data used to improve matching
+    """
+
+    class Meta:
+        """Metaclass options for this mixin.
+
+        Note: abstract must be true, as this is only a mixin, not a separate table
+        """
+        abstract = True
+
+    barcode_data = models.CharField(
+        blank=True, max_length=500,
+        verbose_name=_('Barcode Data'),
+        help_text=_('Third party barcode data'),
+    )
+
+    barcode_hash = models.CharField(
+        blank=True, max_length=128,
+        verbose_name=_('Barcode Hash'),
+        help_text=_('Unique hash of barcode data')
+    )
+
+    @classmethod
+    def barcode_model_type(cls):
+        """Return the model 'type' for creating a custom QR code."""
+
+        # By default, use the name of the class
+        return cls.__name__.lower()
+
+    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
+        )
+
+    @property
+    def barcode(self):
+        """Format a minimal barcode string (e.g. for label printing)"""
+
+        return self.format_barcode(brief=True)
+
+    @classmethod
+    def lookup_barcode(cls, barcode_hash):
+        """Check if a model instance exists with the specified third-party barcode hash."""
+
+        return cls.objects.filter(barcode_hash=barcode_hash).first()
+
+    def assign_barcode(self, barcode_hash=None, barcode_data=None, raise_error=True):
+        """Assign an external (third-party) barcode to this object."""
+
+        # Must provide either barcode_hash or barcode_data
+        if barcode_hash is None and barcode_data is None:
+            raise ValueError("Provide either 'barcode_hash' or 'barcode_data'")
+
+        # If barcode_hash is not provided, create from supplier barcode_data
+        if barcode_hash is None:
+            barcode_hash = InvenTree.helpers.hash_barcode(barcode_data)
+
+        # Check for existing item
+        if self.__class__.lookup_barcode(barcode_hash) is not None:
+            if raise_error:
+                raise ValidationError(_("Existing barcode found"))
+            else:
+                return False
+
+        if barcode_data is not None:
+            self.barcode_data = barcode_data
+
+        self.barcode_hash = barcode_hash
+
+        self.save()
+
+        return True
+
+    def unassign_barcode(self):
+        """Unassign custom barcode from this model"""
+
+        self.barcode_data = ''
+        self.barcode_hash = ''
+
+        self.save()
+
+
 @receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log')
 def before_delete_tree_item(sender, instance, using, **kwargs):
     """Receives pre_delete signal from InvenTreeTree object.
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 823c9a918b..1929ba1325 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -19,6 +19,7 @@ from djmoney.contrib.exchange.models import Rate, convert_money
 from djmoney.money import Money
 
 import InvenTree.format
+import InvenTree.helpers
 import InvenTree.tasks
 from common.models import InvenTreeSetting
 from common.settings import currency_codes
@@ -848,3 +849,32 @@ class TestOffloadTask(helpers.InvenTreeTestCase):
             1, 2, 3, 4, 5,
             force_async=True
         )
+
+
+class BarcodeMixinTest(helpers.InvenTreeTestCase):
+    """Tests for the InvenTreeBarcodeMixin mixin class"""
+
+    def test_barcode_model_type(self):
+        """Test that the barcode_model_type property works for each class"""
+
+        from part.models import Part
+        from stock.models import StockItem, StockLocation
+
+        self.assertEqual(Part.barcode_model_type(), 'part')
+        self.assertEqual(StockItem.barcode_model_type(), 'stockitem')
+        self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation')
+
+    def test_bacode_hash(self):
+        """Test that the barcode hashing function provides correct results"""
+
+        # Test multiple values for the hashing function
+        # This is to ensure that the hash function is always "backwards compatible"
+        hashing_tests = {
+            'abcdefg': '7ac66c0f148de9519b8bd264312c4d64',
+            'ABCDEFG': 'bb747b3df3130fe1ca4afa93fb7d97c9',
+            '1234567': 'fcea920f7412b5da7be0cf42b8c93759',
+            '{"part": 17, "stockitem": 12}': 'c88c11ed0628eb7fef0d59b098b96975',
+        }
+
+        for barcode, hash in hashing_tests.items():
+            self.assertEqual(InvenTree.helpers.hash_barcode(barcode), hash)
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 07bcaf4514..5852fe4965 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -22,7 +22,7 @@ from mptt.exceptions import InvalidMove
 from rest_framework import serializers
 
 from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
-from InvenTree.helpers import increment, normalize, MakeBarcode, notify_responsible
+from InvenTree.helpers import increment, normalize, notify_responsible
 from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
 
 from build.validators import generate_next_build_reference, validate_build_order_reference
@@ -110,17 +110,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
         verbose_name = _("Build Order")
         verbose_name_plural = _("Build Orders")
 
-    def format_barcode(self, **kwargs):
-        """Return a JSON string to represent this build as a barcode."""
-        return MakeBarcode(
-            "buildorder",
-            self.pk,
-            {
-                "reference": self.title,
-                "url": self.get_absolute_url(),
-            }
-        )
-
     @staticmethod
     def filterByDate(queryset, min_date, max_date):
         """Filter by 'minimum and maximum date range'.
diff --git a/InvenTree/company/migrations/0048_auto_20220913_0312.py b/InvenTree/company/migrations/0048_auto_20220913_0312.py
new file mode 100644
index 0000000000..6f4aecc77d
--- /dev/null
+++ b/InvenTree/company/migrations/0048_auto_20220913_0312.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.15 on 2022-09-13 03:12
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('company', '0047_supplierpart_pack_size'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='supplierpart',
+            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='supplierpart',
+            name='barcode_hash',
+            field=models.CharField(blank=True, help_text='Unique hash of barcode data', max_length=128, verbose_name='Barcode Hash'),
+        ),
+    ]
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index aa67de0018..67b48a0e85 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -21,7 +21,7 @@ import InvenTree.helpers
 import InvenTree.validators
 from common.settings import currency_code_default
 from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
-from InvenTree.models import InvenTreeAttachment
+from InvenTree.models import InvenTreeAttachment, InvenTreeBarcodeMixin
 from InvenTree.status_codes import PurchaseOrderStatus
 
 
@@ -391,7 +391,7 @@ class SupplierPartManager(models.Manager):
         )
 
 
-class SupplierPart(models.Model):
+class SupplierPart(InvenTreeBarcodeMixin, models.Model):
     """Represents a unique part as provided by a Supplier Each SupplierPart is identified by a SKU (Supplier Part Number) Each SupplierPart is also linked to a Part or ManufacturerPart object. A Part may be available from multiple suppliers.
 
     Attributes:
diff --git a/InvenTree/company/templates/company/supplier_part.html b/InvenTree/company/templates/company/supplier_part.html
index 205f2da352..9a058c2d4e 100644
--- a/InvenTree/company/templates/company/supplier_part.html
+++ b/InvenTree/company/templates/company/supplier_part.html
@@ -30,6 +30,22 @@
 {% url 'admin:company_supplierpart_change' part.pk as url %}
 {% include "admin_button.html" with url=url %}
 {% endif %}
+{% if barcodes %}
+<!-- Barcode actions menu -->
+<div class='btn-group' role='group'>
+    <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' 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 part.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 %}
+    </ul>
+</div>
+{% endif %}
 {% if roles.purchase_order.change or roles.purchase_order.add or roles.purchase_order.delete %}
 <div class='btn-group'>
     <button id='supplier-part-actions' title='{% trans "Supplier part actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
@@ -100,6 +116,13 @@ src="{% static 'img/blank_image.png' %}"
         <td>{% decimal part.available %}<span class='badge bg-dark rounded-pill float-right'>{% render_date part.availability_updated %}</span></td>
     </tr>
     {% endif %}
+    {% if part.barcode_hash %}
+    <tr>
+        <td><span class='fas fa-barcode'></span></td>
+        <td>{% trans "Barcode Identifier" %}</td>
+        <td {% if part.barcode_data %}title='{{ part.barcode_data }}'{% endif %}>{{ part.barcode_hash }}</td>
+    </tr>
+    {% endif %}
 </table>
 
 {% endblock details %}
@@ -241,6 +264,33 @@ src="{% static 'img/blank_image.png' %}"
 {% block js_ready %}
 {{ block.super }}
 
+{% if barcodes %}
+
+$("#show-qr-code").click(function() {
+    launchModalForm("{% url 'supplier-part-qr' part.pk %}",
+    {
+        no_post: true,
+    });
+});
+
+$("#barcode-link").click(function() {
+    linkBarcodeDialog(
+        {
+            supplierpart: {{ part.pk }},
+        },
+        {
+            title: '{% trans "Link Barcode to Supplier Part" %}',
+        }
+    );
+});
+
+$("#barcode-unlink").click(function() {
+    unlinkBarcode({
+        supplierpart: {{ part.pk }},
+    });
+});
+{% endif %}
+
 function reloadPriceBreaks() {
     $("#price-break-table").bootstrapTable("refresh");
 }
diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py
index 1b91cae5be..34aa85a366 100644
--- a/InvenTree/company/urls.py
+++ b/InvenTree/company/urls.py
@@ -25,5 +25,10 @@ manufacturer_part_urls = [
 ]
 
 supplier_part_urls = [
-    re_path(r'^(?P<pk>\d+)/', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
+    re_path(r'^(?P<pk>\d+)/', include([
+        re_path('^qr_code/?', views.SupplierPartQRCode.as_view(), name='supplier-part-qr'),
+        re_path('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
+    ]))
+
+
 ]
diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py
index 147e5e407d..96411bb493 100644
--- a/InvenTree/company/views.py
+++ b/InvenTree/company/views.py
@@ -4,7 +4,7 @@ from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import DetailView, ListView
 
-from InvenTree.views import InvenTreeRoleMixin
+from InvenTree.views import InvenTreeRoleMixin, QRCodeView
 from plugin.views import InvenTreePluginViewMixin
 
 from .models import Company, ManufacturerPart, SupplierPart
@@ -112,3 +112,18 @@ class SupplierPartDetail(InvenTreePluginViewMixin, DetailView):
     context_object_name = 'part'
     queryset = SupplierPart.objects.all()
     permission_required = 'purchase_order.view'
+
+
+class SupplierPartQRCode(QRCodeView):
+    """View for displaying a QR code for a StockItem object."""
+
+    ajax_form_title = _("Stock Item QR Code")
+    role_required = 'stock.view'
+
+    def get_qr_data(self):
+        """Generate QR code data for the StockItem."""
+        try:
+            part = SupplierPart.objects.get(id=self.pk)
+            return part.format_barcode()
+        except SupplierPart.DoesNotExist:
+            return None
diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py
index 13b5ac331e..ee9ad7f3cb 100644
--- a/InvenTree/label/models.py
+++ b/InvenTree/label/models.py
@@ -249,7 +249,8 @@ class StockItemLabel(LabelTemplate):
             'revision': stock_item.part.revision,
             'quantity': normalize(stock_item.quantity),
             'serial': stock_item.serial,
-            'uid': stock_item.uid,
+            'barcode_data': stock_item.barcode_data,
+            'barcode_hash': stock_item.barcode_hash,
             'qr_data': stock_item.format_barcode(brief=True),
             'qr_url': stock_item.format_barcode(url=True, request=request),
             'tests': stock_item.testResultMap(),
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index e5aaaa298c..94cc4078ec 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -450,11 +450,11 @@ class PurchaseOrder(Order):
         notes = kwargs.get('notes', '')
 
         # Extract optional barcode field
-        barcode = kwargs.get('barcode', None)
+        barcode_hash = kwargs.get('barcode', None)
 
         # Prevent null values for barcode
-        if barcode is None:
-            barcode = ''
+        if barcode_hash is None:
+            barcode_hash = ''
 
         if self.status != PurchaseOrderStatus.PLACED:
             raise ValidationError(
@@ -497,7 +497,7 @@ class PurchaseOrder(Order):
                     batch=batch_code,
                     serial=sn,
                     purchase_price=line.purchase_price,
-                    uid=barcode
+                    barcode_hash=barcode_hash
                 )
 
                 stock.save(add_note=False)
diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py
index 43311db925..6d23e83628 100644
--- a/InvenTree/order/serializers.py
+++ b/InvenTree/order/serializers.py
@@ -497,7 +497,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
         if not barcode or barcode.strip() == '':
             return None
 
-        if stock.models.StockItem.objects.filter(uid=barcode).exists():
+        if stock.models.StockItem.objects.filter(barcode_hash=barcode).exists():
             raise ValidationError(_('Barcode is already in use'))
 
         return barcode
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
index da8011923b..b7d4fec091 100644
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -582,11 +582,11 @@ class PurchaseOrderReceiveTest(OrderTest):
         """Tests for checking in items with invalid barcodes:
 
         - Cannot check in "duplicate" barcodes
-        - Barcodes cannot match UID field for existing StockItem
+        - Barcodes cannot match 'barcode_hash' field for existing StockItem
         """
         # Set stock item barcode
         item = StockItem.objects.get(pk=1)
-        item.uid = 'MY-BARCODE-HASH'
+        item.barcode_hash = 'MY-BARCODE-HASH'
         item.save()
 
         response = self.post(
@@ -705,8 +705,8 @@ class PurchaseOrderReceiveTest(OrderTest):
         self.assertEqual(stock_2.last().location.pk, 2)
 
         # Barcodes should have been assigned to the stock items
-        self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-123').exists())
-        self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
+        self.assertTrue(StockItem.objects.filter(barcode_hash='MY-UNIQUE-BARCODE-123').exists())
+        self.assertTrue(StockItem.objects.filter(barcode_hash='MY-UNIQUE-BARCODE-456').exists())
 
     def test_batch_code(self):
         """Test that we can supply a 'batch code' when receiving items."""
diff --git a/InvenTree/part/migrations/0086_auto_20220912_0007.py b/InvenTree/part/migrations/0086_auto_20220912_0007.py
new file mode 100644
index 0000000000..dfaba36cf2
--- /dev/null
+++ b/InvenTree/part/migrations/0086_auto_20220912_0007.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.15 on 2022-09-12 00:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('part', '0085_partparametertemplate_description'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='part',
+            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='part',
+            name='barcode_hash',
+            field=models.CharField(blank=True, help_text='Unique hash of barcode data', max_length=128, verbose_name='Barcode Hash'),
+        ),
+    ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 03407fd89c..6fe06e49db 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -43,7 +43,7 @@ from InvenTree import helpers, validators
 from InvenTree.fields import InvenTreeNotesField, InvenTreeURLField
 from InvenTree.helpers import decimal2money, decimal2string, normalize
 from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
-                              InvenTreeTree)
+                              InvenTreeBarcodeMixin, InvenTreeTree)
 from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
                                     SalesOrderStatus)
 from order import models as OrderModels
@@ -300,7 +300,7 @@ class PartManager(TreeManager):
 
 
 @cleanup.ignore
-class Part(MetadataMixin, MPTTModel):
+class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
     """The Part object represents an abstract part, the 'concept' of an actual entity.
 
     An actual physical instance of a Part is a StockItem which is treated separately.
@@ -941,18 +941,6 @@ class Part(MetadataMixin, MPTTModel):
 
     responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Responsible'), related_name='parts_responible')
 
-    def format_barcode(self, **kwargs):
-        """Return a JSON string for formatting a barcode for this Part object."""
-        return helpers.MakeBarcode(
-            "part",
-            self.id,
-            {
-                "name": self.full_name,
-                "url": reverse('api-part-detail', kwargs={'pk': self.id}),
-            },
-            **kwargs
-        )
-
     @property
     def category_path(self):
         """Return the category path of this Part instance"""
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 9b09a58521..aef2183740 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -45,6 +45,11 @@
         {% if barcodes %}
         <li><a class='dropdown-item' href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
         {% endif %}
+        {% if part.barcode_hash %}
+        <li><a class='dropdown-item' href='#' id='barcode-unlink'><span class='fas fa-unlink'></span> {% trans "Unink 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 %}
@@ -167,6 +172,7 @@
         <td>{% trans "Description" %}</td>
         <td>{{ part.description }}{% include "clip.html"%}</td>
     </tr>
+
 </table>
 
 <!-- Part info messages -->
@@ -295,6 +301,13 @@
                 <td>{{ part.keywords }}{% include "clip.html"%}</td>
             </tr>
             {% endif %}
+            {% if part.barcode_hash %}
+            <tr>
+                <td><span class='fas fa-barcode'></span></td>
+                <td>{% trans "Barcode Identifier" %}</td>
+                <td {% if part.barcode_data %}title='{{ part.barcode_data }}'{% endif %}>{{ part.barcode_hash }}</td>
+            </tr>
+            {% endif %}
         </table>
         </div>
         <div class='col-sm-6'>
@@ -391,6 +404,7 @@
         }
     );
 
+    {% if barcodes %}
     $("#show-qr-code").click(function() {
         launchModalForm(
             "{% url 'part-qr' part.id %}",
@@ -400,6 +414,24 @@
         );
     });
 
+    $('#barcode-unlink').click(function() {
+        unlinkBarcode({
+            part: {{ part.pk }},
+        });
+    });
+
+    $('#barcode-link').click(function() {
+        linkBarcodeDialog(
+            {
+                part: {{ part.pk }},
+            },
+            {
+                title: '{% trans "Link Barcode to Part" %}',
+            }
+        );
+    });
+    {% endif %}
+
     {% if labels_enabled %}
     $('#print-label').click(function() {
         printPartLabels([{{ part.pk }}]);
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index a33a0ef83b..47b23f9b78 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -144,6 +144,15 @@ class PartTest(TestCase):
 
         Part.objects.rebuild()
 
+    def test_barcode_mixin(self):
+        """Test the barcode mixin functionality"""
+
+        self.assertEqual(Part.barcode_model_type(), 'part')
+
+        p = Part.objects.get(pk=1)
+        barcode = p.format_barcode(brief=True)
+        self.assertEqual(barcode, '{"part": 1}')
+
     def test_tree(self):
         """Test that the part variant tree is working properly"""
         chair = Part.objects.get(pk=10000)
@@ -243,7 +252,7 @@ class PartTest(TestCase):
         """Test barcode format functionality"""
         barcode = self.r1.format_barcode(brief=False)
         self.assertIn('InvenTree', barcode)
-        self.assertIn(self.r1.name, barcode)
+        self.assertIn('"part": {"id": 3}', barcode)
 
     def test_copy(self):
         """Test that we can 'deep copy' a Part instance"""
diff --git a/InvenTree/plugin/base/barcodes/api.py b/InvenTree/plugin/base/barcodes/api.py
index 7cda2576c0..21a35a754a 100644
--- a/InvenTree/plugin/base/barcodes/api.py
+++ b/InvenTree/plugin/base/barcodes/api.py
@@ -1,7 +1,7 @@
 """API endpoints for barcode plugins."""
 
 
-from django.urls import path, re_path, reverse
+from django.urls import path, re_path
 from django.utils.translation import gettext_lazy as _
 
 from rest_framework import permissions
@@ -9,11 +9,10 @@ from rest_framework.exceptions import ValidationError
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
+from InvenTree.helpers import hash_barcode
 from plugin import registry
-from plugin.base.barcodes.mixins import hash_barcode
-from plugin.builtin.barcodes.inventree_barcode import InvenTreeBarcodePlugin
-from stock.models import StockItem
-from stock.serializers import StockItemSerializer
+from plugin.builtin.barcodes.inventree_barcode import (
+    InvenTreeExternalBarcodePlugin, InvenTreeInternalBarcodePlugin)
 
 
 class BarcodeScan(APIView):
@@ -51,85 +50,40 @@ class BarcodeScan(APIView):
         if 'barcode' not in data:
             raise ValidationError({'barcode': _('Must provide barcode_data parameter')})
 
-        plugins = registry.with_mixin('barcode')
+        # Ensure that the default barcode handlers are run first
+        plugins = [
+            InvenTreeInternalBarcodePlugin(),
+            InvenTreeExternalBarcodePlugin(),
+        ] + registry.with_mixin('barcode')
 
         barcode_data = data.get('barcode')
-
-        # Ensure that the default barcode handler is installed
-        plugins.append(InvenTreeBarcodePlugin())
+        barcode_hash = hash_barcode(barcode_data)
 
         # Look for a barcode plugin which knows how to deal with this barcode
         plugin = None
-
-        for current_plugin in plugins:
-            current_plugin.init(barcode_data)
-
-            if current_plugin.validate():
-                plugin = current_plugin
-                break
-
-        match_found = False
         response = {}
 
+        for current_plugin in plugins:
+
+            result = current_plugin.scan(barcode_data)
+
+            if result is not None:
+                plugin = current_plugin
+                response = result
+                break
+
+        response['plugin'] = plugin.name if plugin else None
         response['barcode_data'] = barcode_data
+        response['barcode_hash'] = barcode_hash
 
-        # A plugin has been found!
-        if plugin is not None:
-
-            # Try to associate with a stock item
-            item = plugin.getStockItem()
-
-            if item is None:
-                item = plugin.getStockItemByHash()
-
-            if item is not None:
-                response['stockitem'] = plugin.renderStockItem(item)
-                response['url'] = reverse('stock-item-detail', kwargs={'pk': item.id})
-                match_found = True
-
-            # Try to associate with a stock location
-            loc = plugin.getStockLocation()
-
-            if loc is not None:
-                response['stocklocation'] = plugin.renderStockLocation(loc)
-                response['url'] = reverse('stock-location-detail', kwargs={'pk': loc.id})
-                match_found = True
-
-            # Try to associate with a part
-            part = plugin.getPart()
-
-            if part is not None:
-                response['part'] = plugin.renderPart(part)
-                response['url'] = reverse('part-detail', kwargs={'pk': part.id})
-                match_found = True
-
-            response['hash'] = plugin.hash()
-            response['plugin'] = plugin.name
-
-        # No plugin is found!
-        # However, the hash of the barcode may still be associated with a StockItem!
-        else:
-            result_hash = hash_barcode(barcode_data)
-
-            response['hash'] = result_hash
-            response['plugin'] = None
-
-            # Try to look for a matching StockItem
-            try:
-                item = StockItem.objects.get(uid=result_hash)
-                serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True)
-                response['stockitem'] = serializer.data
-                response['url'] = reverse('stock-item-detail', kwargs={'pk': item.id})
-                match_found = True
-            except StockItem.DoesNotExist:
-                pass
-
-        if not match_found:
+        # A plugin has not been found!
+        if plugin is None:
             response['error'] = _('No match found for barcode data')
+
+            raise ValidationError(response)
         else:
             response['success'] = _('Match found for barcode data')
-
-        return Response(response)
+            return Response(response)
 
 
 class BarcodeAssign(APIView):
@@ -148,97 +102,134 @@ class BarcodeAssign(APIView):
 
         Checks inputs and assign barcode (hash) to StockItem.
         """
+
         data = request.data
 
         if 'barcode' not in data:
             raise ValidationError({'barcode': _('Must provide barcode_data parameter')})
 
-        if 'stockitem' not in data:
-            raise ValidationError({'stockitem': _('Must provide stockitem parameter')})
-
         barcode_data = data['barcode']
 
-        try:
-            item = StockItem.objects.get(pk=data['stockitem'])
-        except (ValueError, StockItem.DoesNotExist):
-            raise ValidationError({'stockitem': _('No matching stock item found')})
+        # Here we only check against 'InvenTree' plugins
+        plugins = [
+            InvenTreeInternalBarcodePlugin(),
+            InvenTreeExternalBarcodePlugin(),
+        ]
 
-        plugins = registry.with_mixin('barcode')
+        # First check if the provided barcode matches an existing database entry
+        for plugin in plugins:
+            result = plugin.scan(barcode_data)
 
-        plugin = None
+            if result is not None:
+                result["error"] = _("Barcode matches existing item")
+                result["plugin"] = plugin.name
+                result["barcode_data"] = barcode_data
 
-        for current_plugin in plugins:
-            current_plugin.init(barcode_data)
+                raise ValidationError(result)
 
-            if current_plugin.validate():
-                plugin = current_plugin
-                break
+        barcode_hash = hash_barcode(barcode_data)
 
-        match_found = False
+        valid_labels = []
 
-        response = {}
+        for model in InvenTreeExternalBarcodePlugin.get_supported_barcode_models():
+            label = model.barcode_model_type()
+            valid_labels.append(label)
 
-        response['barcode_data'] = barcode_data
+            if label in data:
+                try:
+                    instance = model.objects.get(pk=data[label])
 
-        # Matching plugin was found
-        if plugin is not None:
+                    instance.assign_barcode(
+                        barcode_data=barcode_data,
+                        barcode_hash=barcode_hash,
+                    )
 
-            result_hash = plugin.hash()
-            response['hash'] = result_hash
-            response['plugin'] = plugin.name
+                    return Response({
+                        'success': f"Assigned barcode to {label} instance",
+                        label: {
+                            'pk': instance.pk,
+                        },
+                        "barcode_data": barcode_data,
+                        "barcode_hash": barcode_hash,
+                    })
 
-            # Ensure that the barcode does not already match a database entry
+                except (ValueError, model.DoesNotExist):
+                    raise ValidationError({
+                        'error': f"No matching {label} instance found in database",
+                    })
 
-            if plugin.getStockItem() is not None:
-                match_found = True
-                response['error'] = _('Barcode already matches Stock Item')
+        # If we got here, it means that no valid model types were provided
+        raise ValidationError({
+            'error': f"Missing data: provide one of '{valid_labels}'",
+        })
 
-            if plugin.getStockLocation() is not None:
-                match_found = True
-                response['error'] = _('Barcode already matches Stock Location')
 
-            if plugin.getPart() is not None:
-                match_found = True
-                response['error'] = _('Barcode already matches Part')
+class BarcodeUnassign(APIView):
+    """Endpoint for unlinking / unassigning a custom barcode from a database object"""
 
-            if not match_found:
-                item = plugin.getStockItemByHash()
+    permission_classes = [
+        permissions.IsAuthenticated,
+    ]
 
-                if item is not None:
-                    response['error'] = _('Barcode hash already matches Stock Item')
-                    match_found = True
+    def post(self, request, *args, **kwargs):
+        """Respond to a barcode unassign POST request"""
 
-        else:
-            result_hash = hash_barcode(barcode_data)
+        # The following database models support assignment of third-party barcodes
+        supported_models = InvenTreeExternalBarcodePlugin.get_supported_barcode_models()
 
-            response['hash'] = result_hash
-            response['plugin'] = None
+        supported_labels = [model.barcode_model_type() for model in supported_models]
+        model_names = ', '.join(supported_labels)
 
-            # Lookup stock item by hash
-            try:
-                item = StockItem.objects.get(uid=result_hash)
-                response['error'] = _('Barcode hash already matches Stock Item')
-                match_found = True
-            except StockItem.DoesNotExist:
-                pass
+        data = request.data
 
-        if not match_found:
-            response['success'] = _('Barcode associated with Stock Item')
+        matched_labels = []
 
-            # Save the barcode hash
-            item.uid = response['hash']
-            item.save()
+        for label in supported_labels:
+            if label in data:
+                matched_labels.append(label)
 
-            serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True)
-            response['stockitem'] = serializer.data
+        if len(matched_labels) == 0:
+            raise ValidationError({
+                'error': f"Missing data: Provide one of '{model_names}'"
+            })
 
-        return Response(response)
+        if len(matched_labels) > 1:
+            raise ValidationError({
+                'error': f"Multiple conflicting fields: '{model_names}'",
+            })
+
+        # At this stage, we know that we have received a single valid field
+        for model in supported_models:
+            label = model.barcode_model_type()
+
+            if label in data:
+                try:
+                    instance = model.objects.get(pk=data[label])
+                except (ValueError, model.DoesNotExist):
+                    raise ValidationError({
+                        label: _('No match found for provided value')
+                    })
+
+                # Unassign the barcode data from the model instance
+                instance.unassign_barcode()
+
+                return Response({
+                    'success': 'Barcode unassigned from {label} instance',
+                })
+
+        # If we get to this point, something has gone wrong!
+        raise ValidationError({
+            'error': 'Could not unassign barcode',
+        })
 
 
 barcode_api_urls = [
-    # Link a barcode to a part
+    # Link a third-party barcode to an item (e.g. Part / StockItem / etc)
     path('link/', BarcodeAssign.as_view(), name='api-barcode-link'),
 
+    # Unlink a third-pary barcode from an item
+    path('unlink/', BarcodeUnassign.as_view(), name='api-barcode-unlink'),
+
     # Catch-all performs barcode 'scan'
     re_path(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
 ]
diff --git a/InvenTree/plugin/base/barcodes/mixins.py b/InvenTree/plugin/base/barcodes/mixins.py
index 5ba90d7157..5ad4794ebd 100644
--- a/InvenTree/plugin/base/barcodes/mixins.py
+++ b/InvenTree/plugin/base/barcodes/mixins.py
@@ -1,33 +1,8 @@
 """Plugin mixin classes for barcode plugin."""
 
-import hashlib
-import string
-
-from part.serializers import PartSerializer
-from stock.models import StockItem
-from stock.serializers import LocationSerializer, StockItemSerializer
-
-
-def hash_barcode(barcode_data):
-    """Calculate an MD5 hash of barcode data.
-
-    HACK: Remove any 'non printable' characters from the hash,
-          as it seems browers will remove special control characters...
-
-    TODO: Work out a way around this!
-    """
-    barcode_data = str(barcode_data).strip()
-
-    printable_chars = filter(lambda x: x in string.printable, barcode_data)
-
-    barcode_data = ''.join(list(printable_chars))
-
-    result_hash = hashlib.md5(str(barcode_data).encode())
-    return str(result_hash.hexdigest())
-
 
 class BarcodeMixin:
-    """Mixin that enables barcode handeling.
+    """Mixin that enables barcode handling.
 
     Custom barcode plugins should use and extend this mixin as necessary.
     """
@@ -49,72 +24,16 @@ class BarcodeMixin:
         """Does this plugin have everything needed to process a barcode."""
         return True
 
-    def init(self, barcode_data):
-        """Initialize the BarcodePlugin instance.
+    def scan(self, barcode_data):
+        """Scan a barcode against this plugin.
 
-        Args:
-            barcode_data: The raw barcode data
+        This method is explicitly called from the /scan/ API endpoint,
+        and thus it is expected that any barcode which matches this barcode will return a result.
+
+        If this plugin finds a match against the provided barcode, it should return a dict object
+        with the intended result.
+
+        Default return value is None
         """
-        self.data = barcode_data
 
-    def getStockItem(self):
-        """Attempt to retrieve a StockItem associated with this barcode.
-
-        Default implementation returns None
-        """
-        return None  # pragma: no cover
-
-    def getStockItemByHash(self):
-        """Attempt to retrieve a StockItem associated with this barcode, based on the barcode hash."""
-        try:
-            item = StockItem.objects.get(uid=self.hash())
-            return item
-        except StockItem.DoesNotExist:
-            return None
-
-    def renderStockItem(self, item):
-        """Render a stock item to JSON response."""
-        serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True)
-        return serializer.data
-
-    def getStockLocation(self):
-        """Attempt to retrieve a StockLocation associated with this barcode.
-
-        Default implementation returns None
-        """
-        return None  # pragma: no cover
-
-    def renderStockLocation(self, loc):
-        """Render a stock location to a JSON response."""
-        serializer = LocationSerializer(loc)
-        return serializer.data
-
-    def getPart(self):
-        """Attempt to retrieve a Part associated with this barcode.
-
-        Default implementation returns None
-        """
-        return None  # pragma: no cover
-
-    def renderPart(self, part):
-        """Render a part to JSON response."""
-        serializer = PartSerializer(part)
-        return serializer.data
-
-    def hash(self):
-        """Calculate a hash for the barcode data.
-
-        This is supposed to uniquely identify the barcode contents,
-        at least within the bardcode sub-type.
-
-        The default implementation simply returns an MD5 hash of the barcode data,
-        encoded to a string.
-
-        This may be sufficient for most applications, but can obviously be overridden
-        by a subclass.
-        """
-        return hash_barcode(self.data)
-
-    def validate(self):
-        """Default implementation returns False."""
-        return False  # pragma: no cover
+        return None
diff --git a/InvenTree/plugin/base/barcodes/test_barcode.py b/InvenTree/plugin/base/barcodes/test_barcode.py
index 932ae0d463..c847d0f586 100644
--- a/InvenTree/plugin/base/barcodes/test_barcode.py
+++ b/InvenTree/plugin/base/barcodes/test_barcode.py
@@ -52,16 +52,11 @@ class BarcodeAPITest(InvenTreeAPITestCase):
         """
         response = self.postBarcode(self.scan_url, '')
 
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.status_code, 400)
 
         data = response.data
         self.assertIn('error', data)
 
-        self.assertIn('barcode_data', data)
-        self.assertIn('hash', data)
-        self.assertIn('plugin', data)
-        self.assertIsNone(data['plugin'])
-
     def test_find_part(self):
         """Test that we can lookup a part based on ID."""
         response = self.client.post(
@@ -92,8 +87,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
         )
 
         self.assertEqual(response.status_code, 400)
-
-        self.assertEqual(response.data['part'], 'Part does not exist')
+        self.assertIn('error', response.data)
 
     def test_find_stock_item(self):
         """Test that we can lookup a stock item based on ID."""
@@ -125,8 +119,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
         )
 
         self.assertEqual(response.status_code, 400)
-
-        self.assertEqual(response.data['stockitem'], 'Stock item does not exist')
+        self.assertIn('error', response.data)
 
     def test_find_location(self):
         """Test that we can lookup a stock location based on ID."""
@@ -158,37 +151,26 @@ class BarcodeAPITest(InvenTreeAPITestCase):
         )
 
         self.assertEqual(response.status_code, 400)
-
-        self.assertEqual(response.data['stocklocation'], 'Stock location does not exist')
+        self.assertIn('error', response.data)
 
     def test_integer_barcode(self):
         """Test scan of an integer barcode."""
         response = self.postBarcode(self.scan_url, '123456789')
 
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.status_code, 400)
 
         data = response.data
         self.assertIn('error', data)
 
-        self.assertIn('barcode_data', data)
-        self.assertIn('hash', data)
-        self.assertIn('plugin', data)
-        self.assertIsNone(data['plugin'])
-
     def test_array_barcode(self):
         """Test scan of barcode with string encoded array."""
         response = self.postBarcode(self.scan_url, "['foo', 'bar']")
 
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.status_code, 400)
 
         data = response.data
         self.assertIn('error', data)
 
-        self.assertIn('barcode_data', data)
-        self.assertIn('hash', data)
-        self.assertIn('plugin', data)
-        self.assertIsNone(data['plugin'])
-
     def test_barcode_generation(self):
         """Test that a barcode is generated with a scan."""
         item = StockItem.objects.get(pk=522)
@@ -208,7 +190,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
         """Test that a barcode can be associated with a StockItem."""
         item = StockItem.objects.get(pk=522)
 
-        self.assertEqual(len(item.uid), 0)
+        self.assertEqual(len(item.barcode_hash), 0)
 
         barcode_data = 'A-TEST-BARCODE-STRING'
 
@@ -226,14 +208,14 @@ class BarcodeAPITest(InvenTreeAPITestCase):
 
         self.assertIn('success', data)
 
-        result_hash = data['hash']
+        result_hash = data['barcode_hash']
 
         # Read the item out from the database again
         item = StockItem.objects.get(pk=522)
 
-        self.assertEqual(result_hash, item.uid)
+        self.assertEqual(result_hash, item.barcode_hash)
 
-        # Ensure that the same UID cannot be assigned to a different stock item!
+        # Ensure that the same barcode hash cannot be assigned to a different stock item!
         response = self.client.post(
             self.assign_url, format='json',
             data={
diff --git a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
index 52e97ddbd6..1b7594870e 100644
--- a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
+++ b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
@@ -9,8 +9,8 @@ references model objects actually exist in the database.
 
 import json
 
-from rest_framework.exceptions import ValidationError
-
+from company.models import SupplierPart
+from InvenTree.helpers import hash_barcode
 from part.models import Part
 from plugin import InvenTreePlugin
 from plugin.mixins import BarcodeMixin
@@ -18,121 +18,89 @@ from stock.models import StockItem, StockLocation
 
 
 class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
+    """Generic base class for handling InvenTree barcodes"""
+
+    @staticmethod
+    def get_supported_barcode_models():
+        """Returns a list of database models which support barcode functionality"""
+
+        return [
+            Part,
+            StockItem,
+            StockLocation,
+            SupplierPart,
+        ]
+
+    def format_matched_response(self, label, model, instance):
+        """Format a response for the scanned data"""
+
+        response = {
+            'pk': instance.pk
+        }
+
+        # Add in the API URL if available
+        if hasattr(model, 'get_api_url'):
+            response['api_url'] = f"{model.get_api_url()}{instance.pk}/"
+
+        # Add in the web URL if available
+        if hasattr(instance, 'get_absolute_url'):
+            response['web_url'] = instance.get_absolute_url()
+
+        return {label: response}
+
+
+class InvenTreeInternalBarcodePlugin(InvenTreeBarcodePlugin):
     """Builtin BarcodePlugin for matching and generating internal barcodes."""
 
-    NAME = "InvenTreeBarcode"
+    NAME = "InvenTreeInternalBarcode"
 
-    def validate(self):
-        """Validate a barcode.
+    def scan(self, barcode_data):
+        """Scan a barcode against this plugin.
 
-        An "InvenTree" barcode must be a jsonnable-dict with the following tags:
-        {
-            'tool': 'InvenTree',
-            'version': <anything>
-        }
+        Here we are looking for a dict object which contains a reference to a particular InvenTree database object
         """
-        # The data must either be dict or be able to dictified
-        if type(self.data) is dict:
+
+        if type(barcode_data) is dict:
             pass
-        elif type(self.data) is str:
+        elif type(barcode_data) is str:
             try:
-                self.data = json.loads(self.data)
-                if type(self.data) is not dict:
-                    return False
+                barcode_data = json.loads(barcode_data)
             except json.JSONDecodeError:
-                return False
+                return None
         else:
-            return False  # pragma: no cover
+            return None
 
-        # If any of the following keys are in the JSON data,
-        # let's go ahead and assume that the code is a valid InvenTree one...
+        if type(barcode_data) is not dict:
+            return None
 
-        for key in ['tool', 'version', 'InvenTree', 'stockitem', 'stocklocation', 'part']:
-            if key in self.data.keys():
-                return True
-
-        return True
-
-    def getStockItem(self):
-        """Lookup StockItem by 'stockitem' key in barcode data."""
-        for k in self.data.keys():
-            if k.lower() == 'stockitem':
-
-                data = self.data[k]
-
-                pk = None
-
-                # Initially try casting to an integer
+        # Look for various matches. First good match will be returned
+        for model in self.get_supported_barcode_models():
+            label = model.barcode_model_type()
+            if label in barcode_data:
                 try:
-                    pk = int(data)
-                except (TypeError, ValueError):  # pragma: no cover
-                    pk = None
+                    instance = model.objects.get(pk=barcode_data[label])
+                    return self.format_matched_response(label, model, instance)
+                except (ValueError, model.DoesNotExist):
+                    pass
 
-                if pk is None:  # pragma: no cover
-                    try:
-                        pk = self.data[k]['id']
-                    except (AttributeError, KeyError):
-                        raise ValidationError({k: "id parameter not supplied"})
 
-                try:
-                    item = StockItem.objects.get(pk=pk)
-                    return item
-                except (ValueError, StockItem.DoesNotExist):  # pragma: no cover
-                    raise ValidationError({k: "Stock item does not exist"})
+class InvenTreeExternalBarcodePlugin(InvenTreeBarcodePlugin):
+    """Builtin BarcodePlugin for matching arbitrary external barcodes."""
 
-        return None
+    NAME = "InvenTreeExternalBarcode"
 
-    def getStockLocation(self):
-        """Lookup StockLocation by 'stocklocation' key in barcode data."""
-        for k in self.data.keys():
-            if k.lower() == 'stocklocation':
+    def scan(self, barcode_data):
+        """Scan a barcode against this plugin.
 
-                pk = None
+        Here we are looking for a dict object which contains a reference to a particular InvenTree databse object
+        """
 
-                # First try simple integer lookup
-                try:
-                    pk = int(self.data[k])
-                except (TypeError, ValueError):  # pragma: no cover
-                    pk = None
+        for model in self.get_supported_barcode_models():
+            label = model.barcode_model_type()
 
-                if pk is None:  # pragma: no cover
-                    # Lookup by 'id' field
-                    try:
-                        pk = self.data[k]['id']
-                    except (AttributeError, KeyError):
-                        raise ValidationError({k: "id parameter not supplied"})
+            barcode_hash = hash_barcode(barcode_data)
 
-                try:
-                    loc = StockLocation.objects.get(pk=pk)
-                    return loc
-                except (ValueError, StockLocation.DoesNotExist):  # pragma: no cover
-                    raise ValidationError({k: "Stock location does not exist"})
+            instance = model.lookup_barcode(barcode_hash)
 
-        return None
-
-    def getPart(self):
-        """Lookup Part by 'part' key in barcode data."""
-        for k in self.data.keys():
-            if k.lower() == 'part':
-
-                pk = None
-
-                # Try integer lookup first
-                try:
-                    pk = int(self.data[k])
-                except (TypeError, ValueError):  # pragma: no cover
-                    pk = None
-
-                if pk is None:  # pragma: no cover
-                    try:
-                        pk = self.data[k]['id']
-                    except (AttributeError, KeyError):
-                        raise ValidationError({k: 'id parameter not supplied'})
-
-                try:
-                    part = Part.objects.get(pk=pk)
-                    return part
-                except (ValueError, Part.DoesNotExist):  # pragma: no cover
-                    raise ValidationError({k: 'Part does not exist'})
-
-        return None
+            if instance is not None:
+                return self.format_matched_response(label, model, instance)
diff --git a/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
index b3fd51c781..1230794625 100644
--- a/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
+++ b/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
@@ -2,8 +2,8 @@
 
 from django.urls import reverse
 
-from rest_framework import status
-
+import part.models
+import stock.models
 from InvenTree.api_tester import InvenTreeAPITestCase
 
 
@@ -14,21 +14,24 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
         'category',
         'part',
         'location',
-        'stock'
+        'stock',
+        'company',
+        'supplier_part',
     ]
 
-    def test_errors(self):
-        """Test all possible error cases for assigment action."""
+    def test_assign_errors(self):
+        """Test error cases for assigment action."""
 
         def test_assert_error(barcode_data):
-            response = self.client.post(
+            response = self.post(
                 reverse('api-barcode-link'), format='json',
                 data={
                     'barcode': barcode_data,
                     'stockitem': 521
-                }
+                },
+                expected_code=400
             )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
             self.assertIn('error', response.data)
 
         # test with already existing stock
@@ -40,11 +43,358 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
         # test with already existing part location
         test_assert_error('{"part": 10004}')
 
-        # test with hash
-        test_assert_error('{"blbla": 10004}')
+    def assign(self, data, expected_code=None):
+        """Peform a 'barcode assign' request"""
+
+        return self.post(
+            reverse('api-barcode-link'),
+            data=data,
+            expected_code=expected_code
+        )
+
+    def unassign(self, data, expected_code=None):
+        """Perform a 'barcode unassign' request"""
+
+        return self.post(
+            reverse('api-barcode-unlink'),
+            data=data,
+            expected_code=expected_code,
+        )
+
+    def scan(self, data, expected_code=None):
+        """Perform a 'scan' operation"""
+
+        return self.post(
+            reverse('api-barcode-scan'),
+            data=data,
+            expected_code=expected_code
+        )
+
+    def test_unassign_errors(self):
+        """Test various error conditions for the barcode unassign endpoint"""
+
+        # Fail without any fields provided
+        response = self.unassign(
+            {},
+            expected_code=400,
+        )
+
+        self.assertIn('Missing data: Provide one of', str(response.data['error']))
+
+        # Fail with too many fields provided
+        response = self.unassign(
+            {
+                'stockitem': 'abcde',
+                'part': 'abcde',
+            },
+            expected_code=400,
+        )
+
+        self.assertIn('Multiple conflicting fields:', str(response.data['error']))
+
+        # Fail with an invalid StockItem instance
+        response = self.unassign(
+            {
+                'stockitem': 'invalid',
+            },
+            expected_code=400,
+        )
+
+        self.assertIn('No match found', str(response.data['stockitem']))
+
+        # Fail with an invalid Part instance
+        response = self.unassign(
+            {
+                'part': 'invalid',
+            },
+            expected_code=400,
+        )
+
+        self.assertIn('No match found', str(response.data['part']))
+
+    def test_assign_to_stock_item(self):
+        """Test that we can assign a unique barcode to a StockItem object"""
+
+        # Test without providing any fields
+        response = self.assign(
+            {
+                'barcode': 'abcde',
+            },
+            expected_code=400
+        )
+
+        self.assertIn('Missing data:', str(response.data))
+
+        # Provide too many fields
+        response = self.assign(
+            {
+                'barcode': 'abcdefg',
+                'part': 1,
+                'stockitem': 1,
+            },
+            expected_code=200
+        )
+
+        self.assertIn('Assigned barcode to part instance', str(response.data))
+        self.assertEqual(response.data['part']['pk'], 1)
+
+        bc_data = '{"blbla": 10007}'
+
+        # Assign a barcode to a StockItem instance
+        response = self.assign(
+            data={
+                'barcode': bc_data,
+                'stockitem': 521,
+            },
+            expected_code=200,
+        )
+
+        data = response.data
+        self.assertEqual(data['barcode_data'], bc_data)
+        self.assertEqual(data['stockitem']['pk'], 521)
+
+        # Check that the StockItem instance has actually been updated
+        si = stock.models.StockItem.objects.get(pk=521)
+
+        self.assertEqual(si.barcode_data, bc_data)
+        self.assertEqual(si.barcode_hash, "2f5dba5c83a360599ba7665b2a4131c6")
+
+        # Now test that we cannot assign this barcode to something else
+        response = self.assign(
+            data={
+                'barcode': bc_data,
+                'stockitem': 1,
+            },
+            expected_code=400
+        )
+
+        self.assertIn('Barcode matches existing item', str(response.data))
+
+        # Next, test that we can 'unassign' the barcode via the API
+        response = self.unassign(
+            {
+                'stockitem': 521,
+            },
+            expected_code=200,
+        )
+
+        si.refresh_from_db()
+
+        self.assertEqual(si.barcode_data, '')
+        self.assertEqual(si.barcode_hash, '')
+
+    def test_assign_to_part(self):
+        """Test that we can assign a unique barcode to a Part instance"""
+
+        barcode = 'xyz-123'
+
+        # Test that an initial scan yields no results
+        response = self.scan(
+            {
+                'barcode': barcode,
+            },
+            expected_code=400
+        )
+
+        # Attempt to assign to an invalid part ID
+        response = self.assign(
+            {
+                'barcode': barcode,
+                'part': 99999999,
+            },
+            expected_code=400,
+        )
+
+        self.assertIn('No matching part instance found in database', str(response.data))
+
+        # Test assigning to a valid part (should pass)
+        response = self.assign(
+            {
+                'barcode': barcode,
+                'part': 1,
+            },
+            expected_code=200,
+        )
+
+        self.assertEqual(response.data['part']['pk'], 1)
+        self.assertEqual(response.data['success'], 'Assigned barcode to part instance')
+
+        # Check that the Part instance has been updated
+        p = part.models.Part.objects.get(pk=1)
+        self.assertEqual(p.barcode_data, 'xyz-123')
+        self.assertEqual(p.barcode_hash, 'bc39d07e9a395c7b5658c231bf910fae')
+
+        # Scanning the barcode should now reveal the 'Part' instance
+        response = self.scan(
+            {
+                'barcode': barcode,
+            },
+            expected_code=200,
+        )
 
-    def test_scan(self):
-        """Test that a barcode can be scanned."""
-        response = self.client.post(reverse('api-barcode-scan'), format='json', data={'barcode': 'blbla=10004'})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertIn('success', response.data)
+        self.assertEqual(response.data['plugin'], 'InvenTreeExternalBarcode')
+        self.assertEqual(response.data['part']['pk'], 1)
+
+        # Attempting to assign the same barcode to a different part should result in an error
+        response = self.assign(
+            {
+                'barcode': barcode,
+                'part': 2,
+            },
+            expected_code=400,
+        )
+
+        self.assertIn('Barcode matches existing item', str(response.data['error']))
+
+        # Now test that we can unassign the barcode data also
+        response = self.unassign(
+            {
+                'part': 1,
+            },
+            expected_code=200,
+        )
+
+        p.refresh_from_db()
+
+        self.assertEqual(p.barcode_data, '')
+        self.assertEqual(p.barcode_hash, '')
+
+    def test_assign_to_location(self):
+        """Test that we can assign a unique barcode to a StockLocation instance"""
+
+        barcode = '555555555555555555555555'
+
+        # Assign random barcode data to a StockLocation instance
+        response = self.assign(
+            data={
+                'barcode': barcode,
+                'stocklocation': 1,
+            },
+            expected_code=200,
+        )
+
+        self.assertIn('success', response.data)
+        self.assertEqual(response.data['stocklocation']['pk'], 1)
+
+        # Check that the StockLocation instance has been updated
+        loc = stock.models.StockLocation.objects.get(pk=1)
+
+        self.assertEqual(loc.barcode_data, barcode)
+        self.assertEqual(loc.barcode_hash, '4aa63f5e55e85c1f842796bf74896dbb')
+
+        # Check that an error is thrown if we try to assign the same value again
+        response = self.assign(
+            data={
+                'barcode': barcode,
+                'stocklocation': 2,
+            },
+            expected_code=400
+        )
+
+        self.assertIn('Barcode matches existing item', str(response.data['error']))
+
+        # Now, unassign the barcode
+        response = self.unassign(
+            {
+                'stocklocation': 1,
+            },
+            expected_code=200,
+        )
+
+        loc.refresh_from_db()
+        self.assertEqual(loc.barcode_data, '')
+        self.assertEqual(loc.barcode_hash, '')
+
+    def test_scan_third_party(self):
+        """Test scanning of third-party barcodes"""
+
+        # First scanned barcode is for a 'third-party' barcode (which does not exist)
+        response = self.scan({'barcode': 'blbla=10008'}, expected_code=400)
+        self.assertEqual(response.data['error'], 'No match found for barcode data')
+
+        # Next scanned barcode is for a 'third-party' barcode (which does exist)
+        response = self.scan({'barcode': 'blbla=10004'}, expected_code=200)
+
+        self.assertEqual(response.data['barcode_data'], 'blbla=10004')
+        self.assertEqual(response.data['plugin'], 'InvenTreeExternalBarcode')
+
+        # Scan for a StockItem instance
+        si = stock.models.StockItem.objects.get(pk=1)
+
+        for barcode in ['abcde', 'ABCDE', '12345']:
+            si.assign_barcode(barcode_data=barcode)
+
+            response = self.scan(
+                {
+                    'barcode': barcode,
+                },
+                expected_code=200,
+            )
+
+            self.assertIn('success', response.data)
+            self.assertEqual(response.data['stockitem']['pk'], 1)
+
+    def test_scan_inventree(self):
+        """Test scanning of first-party barcodes"""
+
+        # Scan a StockItem object (which does not exist)
+        response = self.scan(
+            {
+                'barcode': '{"stockitem": 5}',
+            },
+            expected_code=400,
+        )
+
+        self.assertIn('No match found for barcode data', str(response.data))
+
+        # Scan a StockItem object (which does exist)
+        response = self.scan(
+            {
+                'barcode': '{"stockitem": 1}',
+            },
+            expected_code=200
+        )
+
+        self.assertIn('success', response.data)
+        self.assertIn('stockitem', response.data)
+        self.assertEqual(response.data['stockitem']['pk'], 1)
+
+        # Scan a StockLocation object
+        response = self.scan(
+            {
+                'barcode': '{"stocklocation": 5}',
+            },
+            expected_code=200,
+        )
+
+        self.assertIn('success', response.data)
+        self.assertEqual(response.data['stocklocation']['pk'], 5)
+        self.assertEqual(response.data['stocklocation']['api_url'], '/api/stock/location/5/')
+        self.assertEqual(response.data['stocklocation']['web_url'], '/stock/location/5/')
+        self.assertEqual(response.data['plugin'], 'InvenTreeInternalBarcode')
+
+        # Scan a Part object
+        response = self.scan(
+            {
+                'barcode': '{"part": 5}'
+            },
+            expected_code=200,
+        )
+
+        self.assertEqual(response.data['part']['pk'], 5)
+
+        # Scan a SupplierPart instance
+        response = self.scan(
+            {
+                'barcode': '{"supplierpart": 1}',
+            },
+            expected_code=200
+        )
+
+        self.assertEqual(response.data['supplierpart']['pk'], 1)
+        self.assertEqual(response.data['plugin'], 'InvenTreeInternalBarcode')
+
+        self.assertIn('success', response.data)
+        self.assertIn('barcode_data', response.data)
+        self.assertIn('barcode_hash', response.data)
diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml
index fb798e74be..103d224f8d 100644
--- a/InvenTree/stock/fixtures/stock.yaml
+++ b/InvenTree/stock/fixtures/stock.yaml
@@ -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
diff --git a/InvenTree/stock/migrations/0084_auto_20220903_0154.py b/InvenTree/stock/migrations/0084_auto_20220903_0154.py
new file mode 100644
index 0000000000..88a3500b4c
--- /dev/null
+++ b/InvenTree/stock/migrations/0084_auto_20220903_0154.py
@@ -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'),
+        ),
+    ]
diff --git a/InvenTree/stock/migrations/0085_auto_20220903_0225.py b/InvenTree/stock/migrations/0085_auto_20220903_0225.py
new file mode 100644
index 0000000000..75dc0fb0b9
--- /dev/null
+++ b/InvenTree/stock/migrations/0085_auto_20220903_0225.py
@@ -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
+        )
+    ]
diff --git a/InvenTree/stock/migrations/0086_remove_stockitem_uid.py b/InvenTree/stock/migrations/0086_remove_stockitem_uid.py
new file mode 100644
index 0000000000..916558fabe
--- /dev/null
+++ b/InvenTree/stock/migrations/0086_remove_stockitem_uid.py
@@ -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',
+        ),
+    ]
diff --git a/InvenTree/stock/migrations/0087_auto_20220912_2341.py b/InvenTree/stock/migrations/0087_auto_20220912_2341.py
new file mode 100644
index 0000000000..af811e071b
--- /dev/null
+++ b/InvenTree/stock/migrations/0087_auto_20220912_2341.py
@@ -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'),
+        ),
+    ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index d015b09aae..3724653eab 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -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',
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 871df5a612..2ab079a5ac 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -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',
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index 197b504e96..c46f39ed39 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -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() {
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html
index 13f153fc65..f56913b761 100644
--- a/InvenTree/stock/templates/stock/location.html
+++ b/InvenTree/stock/templates/stock/location.html
@@ -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 () {
diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py
index 172d94fe52..5e2fd72761 100644
--- a/InvenTree/stock/tests.py
+++ b/InvenTree/stock/tests.py
@@ -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):
diff --git a/InvenTree/templates/js/translated/barcode.js b/InvenTree/templates/js/translated/barcode.js
index 3c2211e710..818be0106b 100644
--- a/InvenTree/templates/js/translated/barcode.js
+++ b/InvenTree/templates/js/translated/barcode.js
@@ -352,19 +352,17 @@ function barcodeScanDialog() {
 
 
 /*
- * Dialog for linking a particular barcode to a stock item.
+ * Dialog for linking a particular barcode to a database model instsance
  */
-function linkBarcodeDialog(stockitem) {
+function linkBarcodeDialog(data, options={}) {
 
     var modal = '#modal-form';
 
     barcodeDialog(
-        '{% trans "Link Barcode to Stock Item" %}',
+        options.title,
         {
             url: '/api/barcode/link/',
-            data: {
-                stockitem: stockitem,
-            },
+            data: data,
             onScan: function() {
 
                 $(modal).modal('hide');
@@ -376,13 +374,13 @@ function linkBarcodeDialog(stockitem) {
 
 
 /*
- * Remove barcode association from a device.
+ * Remove barcode association from a database model instance.
  */
-function unlinkBarcode(stockitem) {
+function unlinkBarcode(data, options={}) {
 
     var html = `<b>{% trans "Unlink Barcode" %}</b><br>`;
 
-    html += '{% trans "This will remove the association between this stock item and the barcode" %}';
+    html += '{% trans "This will remove the link to the associated barcode" %}';
 
     showQuestionDialog(
         '{% trans "Unlink Barcode" %}',
@@ -391,13 +389,10 @@ function unlinkBarcode(stockitem) {
             accept_text: '{% trans "Unlink" %}',
             accept: function() {
                 inventreePut(
-                    `/api/stock/${stockitem}/`,
+                    '/api/barcode/unlink/',
+                    data,
                     {
-                        // Clear the UID field
-                        uid: '',
-                    },
-                    {
-                        method: 'PATCH',
+                        method: 'POST',
                         success: function() {
                             location.reload();
                         },