From 001437e083c6f7fe544777e1bf4a3edee8f3f2fb Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 9 Feb 2022 23:02:09 +1100 Subject: [PATCH] Increased error checking when uploading BOM data --- InvenTree/part/serializers.py | 35 +++++++++++++++++----- InvenTree/part/test_bom_import.py | 49 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index aef0c1ee0f..2ec2c43707 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -878,7 +878,7 @@ class BomExtractSerializer(serializers.Serializer): for row in self.dataset.dict: - error = {} + row_error = {} """ If the "level" column is specified, and this is not a top-level BOM item, ignore the row! @@ -939,26 +939,45 @@ class BomExtractSerializer(serializers.Serializer): part = queryset.first() else: # Multiple matches! - error['part'] = _('Multiple matching parts found') + row_error['part'] = _('Multiple matching parts found') if part is None: - if 'part' not in error: - error['part'] = _('No matching part found') + if 'part' not in row_error: + row_error['part'] = _('No matching part found') else: if part.pk in found_parts: - error['part'] = _('Duplicate part selected') - else: - found_parts.add(part.pk) + row_error['part'] = _("Duplicate part selected") + + elif not part.component: + row_error['part'] = _('Part is not designated as a component') + + found_parts.add(part.pk) row['part'] = part.pk if part is not None else None + """ + Read out the 'quantity' column - check that it is valid + """ + quantity = self.find_matching_data(row, 'quantity', self.dataset.headers) + + if quantity is None: + row_error['quantity'] = _('Quantity not provided') + else: + try: + quantity = Decimal(quantity) + + if quantity <= 0: + row_error['quantity'] = _('Quantity must be greater than zero') + except: + row_error['quantity'] = _('Invalid quantity') + # For each "optional" column, ensure the column names are allocated correctly for field_name in self.OPTIONAL_COLUMNS: if field_name not in row: row[field_name] = self.find_matching_data(row, field_name, self.dataset.headers) rows.append(row) - errors.append(error) + errors.append(row_error) return { 'rows': rows, diff --git a/InvenTree/part/test_bom_import.py b/InvenTree/part/test_bom_import.py index 6bc5019c59..70ab3be5eb 100644 --- a/InvenTree/part/test_bom_import.py +++ b/InvenTree/part/test_bom_import.py @@ -29,8 +29,17 @@ class BomUploadTest(InvenTreeAPITestCase): name='Assembly', description='An assembled part', assembly=True, + component=False, ) + for i in range(10): + Part.objects.create( + name=f"Component {i}", + description="A subcomponent that can be used in a BOM", + component=True, + assembly=False, + ) + self.url = reverse('api-bom-extract') def post_bom(self, filename, file_data, part=None, clear_existing=None, expected_code=None, content_type='text/plain'): @@ -164,3 +173,43 @@ class BomUploadTest(InvenTreeAPITestCase): ) self.assertIn('No data rows found', str(response.data)) + + def test_invalid_data(self): + """ + Upload data which contains errors + """ + + dataset = tablib.Dataset() + + # Only these headers are strictly necessary + dataset.headers = ['part_id', 'quantity'] + + components = Part.objects.filter(component=True) + + for idx, cmp in enumerate(components): + + if idx == 5: + cmp.component = False + cmp.save() + + dataset.append([cmp.pk, idx]) + + # Add a duplicate part too + dataset.append([components.first().pk, 'invalid']) + + response = self.post_bom( + 'test.csv', + bytes(dataset.csv, 'utf8'), + content_type='text/csv', + expected_code=201 + ) + + errors = response.data['errors'] + + self.assertIn('Quantity must be greater than zero', str(errors[0])) + self.assertIn('Part is not designated as a component', str(errors[5])) + self.assertIn('Duplicate part selected', str(errors[-1])) + self.assertIn('Invalid quantity', str(errors[-1])) + + for idx, row in enumerate(response.data['rows'][:-1]): + self.assertEqual(str(row['part']), str(components[idx].pk))