diff --git a/.gitignore b/.gitignore
index 83fef7d272..98610ffea5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@ _tmp.csv
 inventree/label.pdf
 inventree/label.png
 inventree/my_special*
+_tests*.txt
 
 # Sphinx files
 docs/_build
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index d0f10e0e2a..0f5b04f512 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -29,20 +29,12 @@ from stock.models import StockItem, StockLocation
 
 from . import config, helpers, ready, status, version
 from .tasks import offload_task
-from .validators import validate_overage, validate_part_name
+from .validators import validate_overage
 
 
 class ValidatorTest(TestCase):
     """Simple tests for custom field validators."""
 
-    def test_part_name(self):
-        """Test part name validator."""
-        validate_part_name('hello world')
-
-        # Validate with some strange chars
-        with self.assertRaises(django_exceptions.ValidationError):
-            validate_part_name('### <> This | name is not } valid')
-
     def test_overage(self):
         """Test overage validator."""
         validate_overage("100%")
diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
index 0edabd280c..0b8c263be2 100644
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -11,8 +11,6 @@ from django.utils.translation import gettext_lazy as _
 from jinja2 import Template
 from moneyed import CURRENCIES
 
-import common.models
-
 
 def validate_currency_code(code):
     """Check that a given code is a valid currency code."""
@@ -47,50 +45,6 @@ class AllowedURLValidator(validators.URLValidator):
         super().__call__(value)
 
 
-def validate_part_name(value):
-    """Validate the name field for a Part instance
-
-    This function is exposed to any Validation plugins, and thus can be customized.
-    """
-
-    from plugin.registry import registry
-
-    for plugin in registry.with_mixin('validation'):
-        # Run the name through each custom validator
-        # If the plugin returns 'True' we will skip any subsequent validation
-        if plugin.validate_part_name(value):
-            return
-
-
-def validate_part_ipn(value):
-    """Validate the IPN field for a Part instance.
-
-    This function is exposed to any Validation plugins, and thus can be customized.
-
-    If no validation errors are raised, the IPN is also validated against a configurable regex pattern.
-    """
-
-    from plugin.registry import registry
-
-    plugins = registry.with_mixin('validation')
-
-    for plugin in plugins:
-        # Run the IPN through each custom validator
-        # If the plugin returns 'True' we will skip any subsequent validation
-        if plugin.validate_part_ipn(value):
-            return
-
-    # If we get to here, none of the plugins have raised an error
-
-    pattern = common.models.InvenTreeSetting.get_setting('PART_IPN_REGEX')
-
-    if pattern:
-        match = re.search(pattern, value)
-
-        if match is None:
-            raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))
-
-
 def validate_purchase_order_reference(value):
     """Validate the 'reference' field of a PurchaseOrder."""
 
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
index 7080e4e4e3..f37937d7ce 100644
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -950,12 +950,12 @@ class PurchaseOrderReceiveTest(OrderTest):
                 {
                     'line_item': 1,
                     'quantity': 10,
-                    'batch_code': 'abc-123',
+                    'batch_code': 'B-abc-123',
                 },
                 {
                     'line_item': 2,
                     'quantity': 10,
-                    'batch_code': 'xyz-789',
+                    'batch_code': 'B-xyz-789',
                 }
             ],
             'location': 1,
@@ -975,8 +975,8 @@ class PurchaseOrderReceiveTest(OrderTest):
         item_1 = StockItem.objects.filter(supplier_part=line_1.part).first()
         item_2 = StockItem.objects.filter(supplier_part=line_2.part).first()
 
-        self.assertEqual(item_1.batch, 'abc-123')
-        self.assertEqual(item_2.batch, 'xyz-789')
+        self.assertEqual(item_1.batch, 'B-abc-123')
+        self.assertEqual(item_2.batch, 'B-xyz-789')
 
     def test_serial_numbers(self):
         """Test that we can supply a 'serial number' when receiving items."""
@@ -991,13 +991,13 @@ class PurchaseOrderReceiveTest(OrderTest):
                 {
                     'line_item': 1,
                     'quantity': 10,
-                    'batch_code': 'abc-123',
+                    'batch_code': 'B-abc-123',
                     'serial_numbers': '100+',
                 },
                 {
                     'line_item': 2,
                     'quantity': 10,
-                    'batch_code': 'xyz-789',
+                    'batch_code': 'B-xyz-789',
                 }
             ],
             'location': 1,
@@ -1022,7 +1022,7 @@ class PurchaseOrderReceiveTest(OrderTest):
             item = StockItem.objects.get(serial_int=i)
             self.assertEqual(item.serial, str(i))
             self.assertEqual(item.quantity, 1)
-            self.assertEqual(item.batch, 'abc-123')
+            self.assertEqual(item.batch, 'B-abc-123')
 
         # A single stock item (quantity 10) created for the second line item
         items = StockItem.objects.filter(supplier_part=line_2.part)
@@ -1031,7 +1031,7 @@ class PurchaseOrderReceiveTest(OrderTest):
         item = items.first()
 
         self.assertEqual(item.quantity, 10)
-        self.assertEqual(item.batch, 'xyz-789')
+        self.assertEqual(item.batch, 'B-xyz-789')
 
 
 class SalesOrderTest(OrderTest):
@@ -1437,7 +1437,7 @@ class SalesOrderLineItemTest(OrderTest):
         n_parts = Part.objects.filter(salable=True).count()
 
         # List by part
-        for part in Part.objects.filter(salable=True):
+        for part in Part.objects.filter(salable=True)[:3]:
             response = self.get(
                 self.url,
                 {
@@ -1449,7 +1449,7 @@ class SalesOrderLineItemTest(OrderTest):
             self.assertEqual(response.data['count'], n_orders)
 
         # List by order
-        for order in models.SalesOrder.objects.all():
+        for order in models.SalesOrder.objects.all()[:3]:
             response = self.get(
                 self.url,
                 {
diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml
index 762a0bf740..8d461402d1 100644
--- a/InvenTree/part/fixtures/part.yaml
+++ b/InvenTree/part/fixtures/part.yaml
@@ -95,7 +95,7 @@
   pk: 100
   fields:
     name: 'Bob'
-    description: 'Can we build it?'
+    description: 'Can we build it? Yes we can!'
     assembly: true
     salable: true
     purchaseable: false
@@ -112,7 +112,7 @@
   pk: 101
   fields:
     name: 'Assembly'
-    description: 'A high level assembly'
+    description: 'A high level assembly part'
     salable: true
     active: True
     tree_id: 0
@@ -125,7 +125,7 @@
   pk: 10000
   fields:
     name: 'Chair Template'
-    description: 'A chair'
+    description: 'A chair, which is actually just a template part'
     is_template: True
     trackable: true
     salable: true
@@ -139,6 +139,7 @@
   pk: 10001
   fields:
     name: 'Blue Chair'
+    description: 'A variant chair part which is blue'
     variant_of: 10000
     trackable: true
     category: 7
@@ -151,6 +152,7 @@
   pk: 10002
   fields:
     name: 'Red chair'
+    description: 'A variant chair part which is red'
     variant_of: 10000
     IPN: "R.CH"
     trackable: true
@@ -164,6 +166,7 @@
   pk: 10003
   fields:
     name: 'Green chair'
+    description: 'A template chair part which is green'
     variant_of: 10000
     category: 7
     trackable: true
@@ -176,6 +179,7 @@
   pk: 10004
   fields:
     name: 'Green chair variant'
+    description: 'A green chair, which is a variant of the chair template'
     variant_of: 10003
     is_template: true
     category: 7
diff --git a/InvenTree/part/migrations/0001_initial.py b/InvenTree/part/migrations/0001_initial.py
index 0368abd9d0..eeed95ce20 100644
--- a/InvenTree/part/migrations/0001_initial.py
+++ b/InvenTree/part/migrations/0001_initial.py
@@ -49,7 +49,7 @@ class Migration(migrations.Migration):
             name='Part',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name])),
+                ('name', models.CharField(help_text='Part name', max_length=100)),
                 ('variant', models.CharField(blank=True, help_text='Part variant or revision code', max_length=32)),
                 ('description', models.CharField(help_text='Part description', max_length=250)),
                 ('keywords', models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250)),
diff --git a/InvenTree/part/migrations/0006_auto_20190526_1215.py b/InvenTree/part/migrations/0006_auto_20190526_1215.py
index b91cc8bbab..1fbf02407f 100644
--- a/InvenTree/part/migrations/0006_auto_20190526_1215.py
+++ b/InvenTree/part/migrations/0006_auto_20190526_1215.py
@@ -1,6 +1,5 @@
 # Generated by Django 2.2 on 2019-05-26 02:15
 
-import InvenTree.validators
 from django.db import migrations, models
 
 
@@ -14,7 +13,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='part',
             name='name',
-            field=models.CharField(help_text='Part name (must be unique)', max_length=100, unique=True, validators=[InvenTree.validators.validate_part_name]),
+            field=models.CharField(help_text='Part name (must be unique)', max_length=100, unique=True),
         ),
         migrations.AlterUniqueTogether(
             name='part',
diff --git a/InvenTree/part/migrations/0010_auto_20190620_2135.py b/InvenTree/part/migrations/0010_auto_20190620_2135.py
index 2033e2870f..a8bd18c43a 100644
--- a/InvenTree/part/migrations/0010_auto_20190620_2135.py
+++ b/InvenTree/part/migrations/0010_auto_20190620_2135.py
@@ -1,6 +1,5 @@
 # Generated by Django 2.2.2 on 2019-06-20 11:35
 
-import InvenTree.validators
 from django.db import migrations, models
 
 
@@ -14,6 +13,6 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='part',
             name='name',
-            field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name]),
+            field=models.CharField(help_text='Part name', max_length=100),
         ),
     ]
diff --git a/InvenTree/part/migrations/0028_auto_20200203_1007.py b/InvenTree/part/migrations/0028_auto_20200203_1007.py
index eca1788f06..b34715ecbc 100644
--- a/InvenTree/part/migrations/0028_auto_20200203_1007.py
+++ b/InvenTree/part/migrations/0028_auto_20200203_1007.py
@@ -1,6 +1,5 @@
 # Generated by Django 2.2.9 on 2020-02-03 10:07
 
-import InvenTree.validators
 from django.db import migrations, models
 
 
@@ -14,6 +13,6 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='part',
             name='IPN',
-            field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, validators=[InvenTree.validators.validate_part_ipn]),
+            field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100),
         ),
     ]
diff --git a/InvenTree/part/migrations/0048_auto_20200902_1404.py b/InvenTree/part/migrations/0048_auto_20200902_1404.py
index 4f3584f3c4..5b1440c950 100644
--- a/InvenTree/part/migrations/0048_auto_20200902_1404.py
+++ b/InvenTree/part/migrations/0048_auto_20200902_1404.py
@@ -1,7 +1,6 @@
 # Generated by Django 3.0.7 on 2020-09-02 14:04
 
 import InvenTree.fields
-import InvenTree.validators
 
 from django.db import migrations, models
 
@@ -16,7 +15,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='part',
             name='IPN',
-            field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, validators=[InvenTree.validators.validate_part_ipn]),
+            field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True),
         ),
         migrations.AlterField(
             model_name='part',
diff --git a/InvenTree/part/migrations/0061_auto_20210103_2313.py b/InvenTree/part/migrations/0061_auto_20210103_2313.py
index 502ff7c8e2..baf52cc811 100644
--- a/InvenTree/part/migrations/0061_auto_20210103_2313.py
+++ b/InvenTree/part/migrations/0061_auto_20210103_2313.py
@@ -1,7 +1,6 @@
 # Generated by Django 3.0.7 on 2021-01-03 12:13
 
 import InvenTree.fields
-import InvenTree.validators
 from django.db import migrations, models
 import django.db.models.deletion
 import mptt.fields
@@ -19,7 +18,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='part',
             name='IPN',
-            field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, validators=[InvenTree.validators.validate_part_ipn], verbose_name='IPN'),
+            field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, verbose_name='IPN'),
         ),
         migrations.AlterField(
             model_name='part',
@@ -59,7 +58,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='part',
             name='name',
-            field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name], verbose_name='Name'),
+            field=models.CharField(help_text='Part name', max_length=100, verbose_name='Name'),
         ),
         migrations.AlterField(
             model_name='part',
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index cf9fb516d8..1aabf5ffb8 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -6,6 +6,7 @@ import decimal
 import hashlib
 import logging
 import os
+import re
 from datetime import datetime, timedelta
 from decimal import Decimal, InvalidOperation
 
@@ -538,7 +539,60 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
 
         return result
 
-    def validate_serial_number(self, serial: str, stock_item=None, check_duplicates=True, raise_error=False):
+    def validate_name(self, raise_error=True):
+        """Validate the name field for this Part instance
+
+        This function is exposed to any Validation plugins, and thus can be customized.
+        """
+
+        from plugin.registry import registry
+
+        for plugin in registry.with_mixin('validation'):
+            # Run the name through each custom validator
+            # If the plugin returns 'True' we will skip any subsequent validation
+
+            try:
+                result = plugin.validate_part_name(self.name, self)
+                if result:
+                    return
+            except ValidationError as exc:
+                if raise_error:
+                    raise ValidationError({
+                        'name': exc.message,
+                    })
+
+    def validate_ipn(self, raise_error=True):
+        """Ensure that the IPN (internal part number) is valid for this Part"
+
+        - Validation is handled by custom plugins
+        - By default, no validation checks are perfomed
+        """
+
+        from plugin.registry import registry
+
+        for plugin in registry.with_mixin('validation'):
+            try:
+                result = plugin.validate_part_ipn(self.IPN, self)
+
+                if result:
+                    # A "true" result force skips any subsequent checks
+                    break
+            except ValidationError as exc:
+                if raise_error:
+                    raise ValidationError({
+                        'IPN': exc.message
+                    })
+
+        # If we get to here, none of the plugins have raised an error
+        pattern = common.models.InvenTreeSetting.get_setting('PART_IPN_REGEX', '', create=False).strip()
+
+        if pattern:
+            match = re.search(pattern, self.IPN)
+
+            if match is None:
+                raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))
+
+    def validate_serial_number(self, serial: str, stock_item=None, check_duplicates=True, raise_error=False, **kwargs):
         """Validate a serial number against this Part instance.
 
         Note: This function is exposed to any Validation plugins, and thus can be customized.
@@ -570,7 +624,7 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
             for plugin in registry.with_mixin('validation'):
                 # Run the serial number through each custom validator
                 # If the plugin returns 'True' we will skip any subsequent validation
-                if plugin.validate_serial_number(serial):
+                if plugin.validate_serial_number(serial, self):
                     return True
         except ValidationError as exc:
             if raise_error:
@@ -620,7 +674,7 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
         conflicts = []
 
         for serial in serials:
-            if not self.validate_serial_number(serial):
+            if not self.validate_serial_number(serial, part=self):
                 conflicts.append(serial)
 
         return conflicts
@@ -765,6 +819,12 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
         if type(self.IPN) is str:
             self.IPN = self.IPN.strip()
 
+        # Run custom validation for the IPN field
+        self.validate_ipn()
+
+        # Run custom validation for the name field
+        self.validate_name()
+
         if self.trackable:
             for part in self.get_used_in().all():
 
@@ -777,7 +837,6 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
         max_length=100, blank=False,
         help_text=_('Part name'),
         verbose_name=_('Name'),
-        validators=[validators.validate_part_name]
     )
 
     is_template = models.BooleanField(
@@ -821,7 +880,6 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
         max_length=100, blank=True, null=True,
         verbose_name=_('IPN'),
         help_text=_('Internal Part Number'),
-        validators=[validators.validate_part_ipn]
     )
 
     revision = models.CharField(
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index e4ab05ae7f..fedbc55b84 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -130,7 +130,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
             for jj in range(10):
                 Part.objects.create(
                     name=f"Part xyz {jj}_{ii}",
-                    description="A test part",
+                    description="A test part with a description",
                     category=child
                 )
 
@@ -428,8 +428,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
         # Make sure that we get an error if we try to create part in the structural category
         with self.assertRaises(ValidationError):
             part = Part.objects.create(
-                name="Part which shall not be created",
-                description="-",
+                name="-",
+                description="Part which shall not be created",
                 category=structural_category
             )
 
@@ -446,8 +446,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
 
         # Create the test part assigned to a non-structural category
         part = Part.objects.create(
-            name="Part which category will be changed to structural",
-            description="-",
+            name="-",
+            description="Part which category will be changed to structural",
             category=non_structural_category
         )
 
@@ -743,7 +743,7 @@ class PartAPITest(PartAPITestBase):
 
         # First, construct a set of template / variant parts
         master_part = Part.objects.create(
-            name='Master', description='Master part',
+            name='Master', description='Master part which has some variants',
             category=category,
             is_template=True,
         )
@@ -1323,7 +1323,7 @@ class PartCreationTests(PartAPITestBase):
         url = reverse('api-part-list')
 
         name = "Kaltgerätestecker"
-        description = "Gerät"
+        description = "Gerät Kaltgerätestecker strange chars should get through"
 
         data = {
             "name": name,
@@ -1347,7 +1347,7 @@ class PartCreationTests(PartAPITestBase):
                         reverse('api-part-list'),
                         {
                             'name': f'thing_{bom}{img}{params}',
-                            'description': 'Some description',
+                            'description': 'Some long description text for this part',
                             'category': 1,
                             'duplicate': {
                                 'part': 100,
@@ -2474,7 +2474,7 @@ class BomItemTest(InvenTreeAPITestCase):
             # Create a variant part!
             variant = Part.objects.create(
                 name=f"Variant_{ii}",
-                description="A variant part",
+                description="A variant part, with a description",
                 component=True,
                 variant_of=sub_part
             )
@@ -2672,7 +2672,7 @@ class BomItemTest(InvenTreeAPITestCase):
             # Create a variant part
             vp = Part.objects.create(
                 name=f"Var {i}",
-                description="Variant part",
+                description="Variant part description field",
                 variant_of=bom_item.sub_part,
             )
 
diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py
index 266ae94aec..196915e593 100644
--- a/InvenTree/part/test_bom_item.py
+++ b/InvenTree/part/test_bom_item.py
@@ -66,7 +66,7 @@ class BomItemTest(TestCase):
 
     def test_integer_quantity(self):
         """Test integer validation for BomItem."""
-        p = Part.objects.create(name="test", description="d", component=True, trackable=True)
+        p = Part.objects.create(name="test", description="part description", component=True, trackable=True)
 
         # Creation of a BOMItem with a non-integer quantity of a trackable Part should fail
         with self.assertRaises(django_exceptions.ValidationError):
@@ -210,10 +210,10 @@ class BomItemTest(TestCase):
         self.assertEqual(assembly.can_build, 0)
 
         # Create some component items
-        c1 = Part.objects.create(name="C1", description="C1")
-        c2 = Part.objects.create(name="C2", description="C2")
-        c3 = Part.objects.create(name="C3", description="C3")
-        c4 = Part.objects.create(name="C4", description="C4")
+        c1 = Part.objects.create(name="C1", description="Part C1 - this is just the part description")
+        c2 = Part.objects.create(name="C2", description="Part C2 - this is just the part description")
+        c3 = Part.objects.create(name="C3", description="Part C3 - this is just the part description")
+        c4 = Part.objects.create(name="C4", description="Part C4 - this is just the part description")
 
         for p in [c1, c2, c3, c4]:
             # Ensure we have stock
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index 92178ae728..d6b191be13 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -169,7 +169,7 @@ class PartTest(TestCase):
     def test_str(self):
         """Test string representation of a Part"""
         p = Part.objects.get(pk=100)
-        self.assertEqual(str(p), "BOB | Bob | A2 - Can we build it?")
+        self.assertEqual(str(p), "BOB | Bob | A2 - Can we build it? Yes we can!")
 
     def test_duplicate(self):
         """Test that we cannot create a "duplicate" Part."""
diff --git a/InvenTree/part/test_pricing.py b/InvenTree/part/test_pricing.py
index 5fa658fe54..dcd2fdc4aa 100644
--- a/InvenTree/part/test_pricing.py
+++ b/InvenTree/part/test_pricing.py
@@ -215,7 +215,7 @@ class PartPricingTests(InvenTreeTestCase):
         # Create a part
         p = part.models.Part.objects.create(
             name='Test part for pricing',
-            description='hello world',
+            description='hello world, this is a part description',
         )
 
         # Create some stock items
diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py
index f8280e9bd8..2551f00356 100644
--- a/InvenTree/plugin/base/integration/mixins.py
+++ b/InvenTree/plugin/base/integration/mixins.py
@@ -9,6 +9,8 @@ from django.urls import include, re_path
 import requests
 
 import InvenTree.helpers
+import part.models
+import stock.models
 from plugin.helpers import (MixinImplementationError, MixinNotImplementedError,
                             render_template, render_text)
 from plugin.models import PluginConfig, PluginSetting
@@ -245,42 +247,45 @@ class ValidationMixin:
         super().__init__()
         self.add_mixin('validation', True, __class__)
 
-    def validate_part_name(self, name: str):
+    def validate_part_name(self, name: str, part: part.models.Part):
         """Perform validation on a proposed Part name
 
         Arguments:
             name: The proposed part name
+            part: The part instance we are validating against
 
         Returns:
-            None or True
+            None or True (refer to class docstring)
 
         Raises:
             ValidationError if the proposed name is objectionable
         """
         return None
 
-    def validate_part_ipn(self, ipn: str):
+    def validate_part_ipn(self, ipn: str, part: part.models.Part):
         """Perform validation on a proposed Part IPN (internal part number)
 
         Arguments:
             ipn: The proposed part IPN
+            part: The Part instance we are validating against
 
         Returns:
-            None or True
+            None or True (refer to class docstring)
 
         Raises:
             ValidationError if the proposed IPN is objectionable
         """
         return None
 
-    def validate_batch_code(self, batch_code: str):
+    def validate_batch_code(self, batch_code: str, item: stock.models.StockItem):
         """Validate the supplied batch code
 
         Arguments:
             batch_code: The proposed batch code (string)
+            item: The StockItem instance we are validating against
 
         Returns:
-            None or True
+            None or True (refer to class docstring)
 
         Raises:
             ValidationError if the proposed batch code is objectionable
@@ -295,14 +300,15 @@ class ValidationMixin:
         """
         return None
 
-    def validate_serial_number(self, serial: str):
-        """Validate the supplied serial number
+    def validate_serial_number(self, serial: str, part: part.models.Part):
+        """Validate the supplied serial number.
 
         Arguments:
             serial: The proposed serial number (string)
+            part: The Part instance for which this serial number is being validated
 
         Returns:
-            None or True
+            None or True (refer to class docstring)
 
         Raises:
             ValidationError if the proposed serial is objectionable
diff --git a/InvenTree/plugin/samples/integration/validation_sample.py b/InvenTree/plugin/samples/integration/validation_sample.py
index aac6b0cd8e..a889dcdfc7 100644
--- a/InvenTree/plugin/samples/integration/validation_sample.py
+++ b/InvenTree/plugin/samples/integration/validation_sample.py
@@ -9,13 +9,16 @@ from plugin.mixins import SettingsMixin, ValidationMixin
 
 
 class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
-    """A sample plugin class for demonstrating custom validation functions"""
+    """A sample plugin class for demonstrating custom validation functions.
+
+    Simple of examples of custom validator code.
+    """
 
     NAME = "CustomValidator"
     SLUG = "validator"
     TITLE = "Custom Validator Plugin"
     DESCRIPTION = "A sample plugin for demonstrating custom validation functionality"
-    VERSION = "0.1"
+    VERSION = "0.2"
 
     SETTINGS = {
         'ILLEGAL_PART_CHARS': {
@@ -35,15 +38,30 @@ class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
             'default': False,
             'validator': bool,
         },
+        'SERIAL_MUST_MATCH_PART': {
+            'name': 'Serial must match part',
+            'description': 'First letter of serial number must match first letter of part name',
+            'default': False,
+            'validator': bool,
+        },
         'BATCH_CODE_PREFIX': {
             'name': 'Batch prefix',
             'description': 'Required prefix for batch code',
-            'default': '',
-        }
+            'default': 'B',
+        },
     }
 
-    def validate_part_name(self, name: str):
-        """Validate part name"""
+    def validate_part_name(self, name: str, part):
+        """Custom validation for Part name field:
+
+        - Name must be shorter than the description field
+        - Name cannot contain illegal characters
+
+        These examples are silly, but serve to demonstrate how the feature could be used
+        """
+
+        if len(part.description) < len(name):
+            raise ValidationError("Part description cannot be shorter than the name")
 
         illegal_chars = self.get_setting('ILLEGAL_PART_CHARS')
 
@@ -51,26 +69,41 @@ class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
             if c in name:
                 raise ValidationError(f"Illegal character in part name: '{c}'")
 
-    def validate_part_ipn(self, ipn: str):
-        """Validate part IPN"""
+    def validate_part_ipn(self, ipn: str, part):
+        """Validate part IPN
+
+        These examples are silly, but serve to demonstrate how the feature could be used
+        """
 
         if self.get_setting('IPN_MUST_CONTAIN_Q') and 'Q' not in ipn:
             raise ValidationError("IPN must contain 'Q'")
 
-    def validate_serial_number(self, serial: str):
-        """Validate serial number for a given StockItem"""
+    def validate_serial_number(self, serial: str, part):
+        """Validate serial number for a given StockItem
+
+        These examples are silly, but serve to demonstrate how the feature could be used
+        """
 
         if self.get_setting('SERIAL_MUST_BE_PALINDROME'):
             if serial != serial[::-1]:
                 raise ValidationError("Serial must be a palindrome")
 
-    def validate_batch_code(self, batch_code: str):
-        """Ensure that a particular batch code meets specification"""
+        if self.get_setting('SERIAL_MUST_MATCH_PART'):
+            # Serial must start with the same letter as the linked part, for some reason
+            if serial[0] != part.name[0]:
+                raise ValidationError("Serial number must start with same letter as part")
+
+    def validate_batch_code(self, batch_code: str, item):
+        """Ensure that a particular batch code meets specification.
+
+        These examples are silly, but serve to demonstrate how the feature could be used
+        """
 
         prefix = self.get_setting('BATCH_CODE_PREFIX')
 
-        if not batch_code.startswith(prefix):
-            raise ValidationError(f"Batch code must start with '{prefix}'")
+        if len(batch_code) > 0:
+            if prefix and not batch_code.startswith(prefix):
+                raise ValidationError(f"Batch code must start with '{prefix}'")
 
     def generate_batch_code(self):
         """Generate a new batch code."""
diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml
index 00d1b1ef4f..99ca9cacea 100644
--- a/InvenTree/stock/fixtures/stock.yaml
+++ b/InvenTree/stock/fixtures/stock.yaml
@@ -81,7 +81,7 @@
   pk: 102
   fields:
     part: 25
-    batch: 'ABCDE'
+    batch: 'BCDE'
     location: 7
     quantity: 0
     level: 0
@@ -109,7 +109,7 @@
     part: 10001
     location: 7
     quantity: 5
-    batch: "AAA"
+    batch: "BBAAA"
     level: 0
     tree_id: 0
     lft: 0
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index 4dc95c313c..2f25b84354 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -529,7 +529,7 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, common.models.MetaMixin, M
 
         for plugin in registry.with_mixin('validation'):
             try:
-                plugin.validate_batch_code(self.batch)
+                plugin.validate_batch_code(self.batch, self)
             except ValidationError as exc:
                 raise ValidationError({
                     'batch': exc.message
@@ -560,6 +560,7 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, common.models.MetaMixin, M
         if type(self.batch) is str:
             self.batch = self.batch.strip()
 
+        # Custom validation of batch code
         self.validate_batch_code()
 
         try:
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index 31a8d514fe..68fb75baaf 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -161,7 +161,7 @@ class StockLocationTest(StockAPITestCase):
             # Create stock items in the location to be deleted
             for jj in range(3):
                 stock_items.append(StockItem.objects.create(
-                    batch=f"Stock Item xyz {jj}",
+                    batch=f"Batch xyz {jj}",
                     location=stock_location_to_delete,
                     part=part
                 ))
@@ -180,7 +180,7 @@ class StockLocationTest(StockAPITestCase):
                 # Create stock items in the sub locations
                 for jj in range(3):
                     child_stock_locations_items.append(StockItem.objects.create(
-                        batch=f"Stock item in sub location xyz {jj}",
+                        batch=f"B xyz {jj}",
                         part=part,
                         location=child
                     ))
@@ -272,7 +272,7 @@ class StockLocationTest(StockAPITestCase):
 
         # Create the test stock item located to a non-structural category
         item = StockItem.objects.create(
-            batch="Item which will be tried to relocated to a structural location",
+            batch="BBB",
             location=non_structural_location,
             part=part
         )
@@ -951,7 +951,7 @@ class StockItemTest(StockAPITestCase):
 
         # First, construct a set of template / variant parts
         master_part = part.models.Part.objects.create(
-            name='Master', description='Master part',
+            name='Master', description='Master part which has variants',
             category=category,
             is_template=True,
         )
diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py
index d30c121ccf..675cadadd2 100644
--- a/InvenTree/stock/tests.py
+++ b/InvenTree/stock/tests.py
@@ -181,8 +181,8 @@ class StockTest(StockTestBase):
         # Ensure that 'global uniqueness' setting is enabled
         InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', True, self.user)
 
-        part_a = Part.objects.create(name='A', description='A', trackable=True)
-        part_b = Part.objects.create(name='B', description='B', trackable=True)
+        part_a = Part.objects.create(name='A', description='A part with a description', trackable=True)
+        part_b = Part.objects.create(name='B', description='B part with a description', trackable=True)
 
         # Create a StockItem for part_a
         StockItem.objects.create(
@@ -577,10 +577,13 @@ class StockTest(StockTestBase):
         """Tests for stock serialization."""
         p = Part.objects.create(
             name='trackable part',
-            description='trackable part',
+            description='A trackable part which can be tracked',
             trackable=True,
         )
 
+        # Ensure we do not have unique serials enabled
+        InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, None)
+
         item = StockItem.objects.create(
             part=p,
             quantity=1,
@@ -608,7 +611,7 @@ class StockTest(StockTestBase):
         """Unit tests for "large" serial numbers which exceed integer encoding."""
         p = Part.objects.create(
             name='trackable part',
-            description='trackable part',
+            description='A trackable part with really big serial numbers',
             trackable=True,
         )
 
@@ -721,6 +724,9 @@ class StockTest(StockTestBase):
 
         self.assertEqual(item.quantity, 10)
 
+        # Ensure we do not have unique serials enabled
+        InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, None)
+
         item.serializeStock(3, [1, 2, 3], self.user)
 
         self.assertEqual(item.quantity, 7)
@@ -1087,8 +1093,14 @@ class TestResultTest(StockTestBase):
         item.pk = None
         item.serial = None
         item.quantity = 50
-        item.batch = "B344"
 
+        # Try with an invalid batch code (according to sample validatoin plugin)
+        item.batch = "X234"
+
+        with self.assertRaises(ValidationError):
+            item.save()
+
+        item.batch = "B123"
         item.save()
 
         # Do some tests!