From 9a9ed5f192df804878a90daae745b5ba352af379 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 Apr 2022 00:36:30 +1000 Subject: [PATCH 01/11] Fix validation of duplicate IPN - Duplicate IPN check does not apply if an empty IPN value is set - Note that "if x" is a more pythonic test than "if x not in [None, '']" --- InvenTree/part/models.py | 7 ++++++- InvenTree/stock/models.py | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b7269f3e5e..35872c09ba 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 self.IPN: + self.IPN = self.IPN.strip() + if self.trackable: for part in self.get_used_in().all(): diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 43593d3283..12131b5b58 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 self.serial: self.serial = self.serial.strip() - if self.batch is not None and type(self.batch) is str: + # Strip batch code field + if self.batch: self.batch = self.batch.strip() try: From a7c18891b553936a3c5b8afad003a201e80dbc4a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 Apr 2022 00:38:08 +1000 Subject: [PATCH 02/11] Increase unit testing for duplicate IPN testing - IPN duplication test is case sensitive! --- InvenTree/part/models.py | 2 +- InvenTree/part/test_part.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 35872c09ba..7973e408ce 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -779,7 +779,7 @@ class Part(MPTTModel): # 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 = Part.objects.filter(IPN=self.IPN) parts = parts.exclude(pk=self.pk) if parts.exists(): diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 040b2c9e68..889259d183 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -348,6 +348,26 @@ class PartSettingsTest(TestCase): with self.assertRaises(ValidationError): 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 sensitive, so other variations don't error out: + Part.objects.create(name='xyz', revision='2', description='A part', IPN='UNIQUe') + Part.objects.create(name='xyz', revision='3', description='A part', IPN='UNIQuE') + Part.objects.create(name='xyz', revision='4', description='A part', IPN='UNIqUE') + + 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): From aa4df62ac97ce4eb639b03b3e6b82d892d52e728 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 Apr 2022 00:40:23 +1000 Subject: [PATCH 03/11] IPN fix --- InvenTree/part/test_part.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 889259d183..cb932993dc 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -348,7 +348,7 @@ class PartSettingsTest(TestCase): with self.assertRaises(ValidationError): 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') From 292d28d3786c6d8179736db51f029f54ef3afb80 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 Apr 2022 01:00:02 +1000 Subject: [PATCH 04/11] Account for cases where serial number could be an integer! --- InvenTree/stock/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 12131b5b58..fa343e9a9c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -454,11 +454,11 @@ class StockItem(MPTTModel): super().clean() # Strip serial number field - if self.serial: + if type(self.serial) is str: self.serial = self.serial.strip() # Strip batch code field - if self.batch: + if type(self.batch) is str: self.batch = self.batch.strip() try: From 5fde9f552c8792b3889401f51eff545e6466e129 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 Apr 2022 01:00:38 +1000 Subject: [PATCH 05/11] Add similar check for IPN --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7973e408ce..1cb83b839a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -800,7 +800,7 @@ class Part(MPTTModel): super().clean() # Strip IPN field - if self.IPN: + if type(self.IPN) is str: self.IPN = self.IPN.strip() if self.trackable: From dc0f18d21fdacaf30c1693c06984b99d9128fdf4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 22 Apr 2022 11:20:49 +1000 Subject: [PATCH 06/11] Specify charset and collation options for the test database --- InvenTree/InvenTree/settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 5b3d2eb8d8..c7e058b26c 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -546,6 +546,12 @@ 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', + 'COLLATION': 'utf8_general_ci', +} + DATABASES = { 'default': db_config } From 2f1e869351f27556cb0451882b4ee625bbcf0a7d Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 22 Apr 2022 11:31:51 +1000 Subject: [PATCH 07/11] Only set collation option for mysql test database --- InvenTree/InvenTree/settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index c7e058b26c..e1c584362f 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -549,14 +549,16 @@ db_config['OPTIONS'] = db_options # Set testing options for the database db_config['TEST'] = { 'CHARSET': 'utf8', - 'COLLATION': 'utf8_general_ci', } +# 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( From 0806e18aca85590aff900ce11edd8e8ec1da6b58 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 22 Apr 2022 12:39:42 +1000 Subject: [PATCH 08/11] Case sensitive collation --- InvenTree/InvenTree/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index e1c584362f..1d80250fb3 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -553,7 +553,7 @@ db_config['TEST'] = { # Set collation option for mysql test database if 'mysql' in db_engine: - db_config['TEST']['COLLATION'] = 'utf8_general_ci' + db_config['TEST']['COLLATION'] = 'utf8' DATABASES = { 'default': db_config From fa3c24b085fff8b7ab765ab74c37b6272f32460f Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 22 Apr 2022 13:25:22 +1000 Subject: [PATCH 09/11] Try a different collation option --- InvenTree/InvenTree/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 1d80250fb3..27553767d7 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -553,7 +553,7 @@ db_config['TEST'] = { # Set collation option for mysql test database if 'mysql' in db_engine: - db_config['TEST']['COLLATION'] = 'utf8' + db_config['TEST']['COLLATION'] = 'utf8mb4' DATABASES = { 'default': db_config From 49949201199b3ffa73d0051c1b3aad6ddb596370 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 22 Apr 2022 13:48:15 +1000 Subject: [PATCH 10/11] Use recommended collation option --- InvenTree/InvenTree/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 27553767d7..c34f101180 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -553,7 +553,8 @@ db_config['TEST'] = { # Set collation option for mysql test database if 'mysql' in db_engine: - db_config['TEST']['COLLATION'] = 'utf8mb4' + # Ref: https://docs.djangoproject.com/en/4.0/ref/databases/#collation-settings + db_config['TEST']['COLLATION'] = 'utf8_bin' DATABASES = { 'default': db_config From 99718865c0dd8ceca179d42ed6313a45e56379bf Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 22 Apr 2022 14:33:46 +1000 Subject: [PATCH 11/11] Further attempts to fix CI issues --- InvenTree/InvenTree/settings.py | 3 +-- InvenTree/part/models.py | 2 +- InvenTree/part/test_part.py | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index c34f101180..e1c584362f 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -553,8 +553,7 @@ db_config['TEST'] = { # Set collation option for mysql test database if 'mysql' in db_engine: - # Ref: https://docs.djangoproject.com/en/4.0/ref/databases/#collation-settings - db_config['TEST']['COLLATION'] = 'utf8_bin' + db_config['TEST']['COLLATION'] = 'utf8_general_ci' DATABASES = { 'default': db_config diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1cb83b839a..d6ca9f650c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -779,7 +779,7 @@ class Part(MPTTModel): # 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=self.IPN) + parts = Part.objects.filter(IPN__iexact=self.IPN) parts = parts.exclude(pk=self.pk) if parts.exists(): diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index cb932993dc..811acebc69 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -352,10 +352,10 @@ class PartSettingsTest(TestCase): # Any duplicate IPN should raise an error Part.objects.create(name='xyz', revision='1', description='A part', IPN='UNIQUE') - # Case sensitive, so other variations don't error out: - Part.objects.create(name='xyz', revision='2', description='A part', IPN='UNIQUe') - Part.objects.create(name='xyz', revision='3', description='A part', IPN='UNIQuE') - Part.objects.create(name='xyz', revision='4', 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')