diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 5b3d2eb8d8..e1c584362f 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -546,11 +546,19 @@ if "sqlite" in db_engine: # Provide OPTIONS dict back to the database configuration dict db_config['OPTIONS'] = db_options +# Set testing options for the database +db_config['TEST'] = { + 'CHARSET': 'utf8', +} + +# Set collation option for mysql test database +if 'mysql' in db_engine: + db_config['TEST']['COLLATION'] = 'utf8_general_ci' + DATABASES = { 'default': db_config } - _cache_config = CONFIG.get("cache", {}) _cache_host = _cache_config.get("host", os.getenv("INVENTREE_CACHE_HOST")) _cache_port = _cache_config.get( diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b7269f3e5e..d6ca9f650c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -777,7 +777,8 @@ class Part(MPTTModel): # User can decide whether duplicate IPN (Internal Part Number) values are allowed allow_duplicate_ipn = common.models.InvenTreeSetting.get_setting('PART_ALLOW_DUPLICATE_IPN') - if self.IPN is not None and not allow_duplicate_ipn: + # Raise an error if an IPN is set, and it is a duplicate + if self.IPN and not allow_duplicate_ipn: parts = Part.objects.filter(IPN__iexact=self.IPN) parts = parts.exclude(pk=self.pk) @@ -798,6 +799,10 @@ class Part(MPTTModel): super().clean() + # Strip IPN field + if type(self.IPN) is str: + self.IPN = self.IPN.strip() + if self.trackable: for part in self.get_used_in().all(): diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 040b2c9e68..811acebc69 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -349,6 +349,26 @@ class PartSettingsTest(TestCase): part = Part(name='Hello', description='A thing', IPN='IPN123', revision='C') part.full_clean() + # Any duplicate IPN should raise an error + Part.objects.create(name='xyz', revision='1', description='A part', IPN='UNIQUE') + + # Case insensitive, so variations on spelling should throw an error + for ipn in ['UNiquE', 'uniQuE', 'unique']: + with self.assertRaises(ValidationError): + Part.objects.create(name='xyz', revision='2', description='A part', IPN=ipn) + + with self.assertRaises(ValidationError): + Part.objects.create(name='zyx', description='A part', IPN='UNIQUE') + + # However, *blank* / empty IPN values should be allowed, even if duplicates are not + # Note that leading / trailling whitespace characters are trimmed, too + Part.objects.create(name='abc', revision='1', description='A part', IPN=None) + Part.objects.create(name='abc', revision='2', description='A part', IPN='') + Part.objects.create(name='abc', revision='3', description='A part', IPN=None) + Part.objects.create(name='abc', revision='4', description='A part', IPN=' ') + Part.objects.create(name='abc', revision='5', description='A part', IPN=' ') + Part.objects.create(name='abc', revision='6', description='A part', IPN=' ') + class PartSubscriptionTests(TestCase): diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 43593d3283..fa343e9a9c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -453,10 +453,12 @@ class StockItem(MPTTModel): super().clean() - if self.serial is not None and type(self.serial) is str: + # Strip serial number field + if type(self.serial) is str: self.serial = self.serial.strip() - if self.batch is not None and type(self.batch) is str: + # Strip batch code field + if type(self.batch) is str: self.batch = self.batch.strip() try: