InvenTree/InvenTree/part/test_bom_import.py

347 lines
8.6 KiB
Python
Raw Normal View History

"""
Unit testing for BOM upload / import functionality
"""
import tablib
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase
from part.models import Part
class BomUploadTest(InvenTreeAPITestCase):
"""
Test BOM file upload API endpoint
"""
roles = [
'part.add',
'part.change',
]
def setUp(self):
super().setUp()
self.part = Part.objects.create(
name='Assembly',
description='An assembled part',
assembly=True,
component=False,
)
for i in range(10):
Part.objects.create(
name=f"Component {i}",
IPN=f"CMP_{i}",
description="A subcomponent that can be used in a BOM",
component=True,
assembly=False,
)
def post_bom(self, filename, file_data, part=None, clear_existing=None, expected_code=None, content_type='text/plain'):
bom_file = SimpleUploadedFile(
filename,
file_data,
content_type=content_type,
)
if part is None:
part = self.part.pk
if clear_existing is None:
clear_existing = False
response = self.post(
2022-02-16 11:19:02 +00:00
reverse('api-bom-import-upload'),
data={
2022-02-16 11:19:02 +00:00
'data_file': bom_file,
},
expected_code=expected_code,
format='multipart',
)
return response
def test_missing_file(self):
"""
POST without a file
"""
response = self.post(
2022-02-16 11:19:02 +00:00
reverse('api-bom-import-upload'),
data={},
expected_code=400
)
2022-02-16 11:19:02 +00:00
self.assertIn('No file was submitted', str(response.data['data_file']))
def test_unsupported_file(self):
"""
POST with an unsupported file type
"""
response = self.post_bom(
'sample.txt',
b'hello world',
expected_code=400,
)
2022-02-16 11:19:02 +00:00
self.assertIn('Unsupported file type', str(response.data['data_file']))
def test_broken_file(self):
"""
Test upload with broken (corrupted) files
"""
response = self.post_bom(
'sample.csv',
b'',
expected_code=400,
)
2022-02-16 11:19:02 +00:00
self.assertIn('The submitted file is empty', str(response.data['data_file']))
response = self.post_bom(
'test.xls',
b'hello world',
expected_code=400,
content_type='application/xls',
)
2022-02-16 11:19:02 +00:00
self.assertIn('Unsupported format, or corrupt file', str(response.data['data_file']))
2022-02-16 11:19:02 +00:00
def test_missing_rows(self):
"""
2022-02-16 11:19:02 +00:00
Test upload of an invalid file (without data rows)
"""
dataset = tablib.Dataset()
dataset.headers = [
'apple',
'banana',
]
response = self.post_bom(
'test.csv',
bytes(dataset.csv, 'utf8'),
content_type='text/csv',
expected_code=400,
)
2022-02-16 11:19:02 +00:00
self.assertIn('No data rows found in file', str(response.data))
# Try again, with an .xlsx file
response = self.post_bom(
'bom.xlsx',
dataset.xlsx,
content_type='application/xlsx',
expected_code=400,
)
2022-02-16 11:19:02 +00:00
self.assertIn('No data rows found in file', str(response.data))
def test_missing_columns(self):
"""
Upload extracted data, but with missing columns
"""
url = reverse('api-bom-import-extract')
2022-02-17 02:07:16 +00:00
rows = [
['1', 'test'],
['2', 'test'],
]
# Post without columns
response = self.post(
url,
{},
expected_code=400,
)
self.assertIn('This field is required', str(response.data['rows']))
self.assertIn('This field is required', str(response.data['columns']))
response = self.post(
url,
{
'rows': rows,
'columns': ['part', 'reference'],
},
expected_code=400
)
self.assertIn("Missing required column: 'quantity'", str(response.data))
response = self.post(
url,
{
'rows': rows,
'columns': ['quantity', 'reference'],
},
expected_code=400,
)
self.assertIn('No part column specified', str(response.data))
response = self.post(
url,
{
'rows': rows,
'columns': ['quantity', 'part'],
},
expected_code=201,
)
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])
2022-02-17 01:10:48 +00:00
url = reverse('api-bom-import-extract')
2022-02-17 01:10:48 +00:00
response = self.post(
url,
{
'columns': dataset.headers,
'rows': [row for row in dataset],
},
)
2022-02-17 01:10:48 +00:00
rows = response.data['rows']
# Returned data must be the same as the original dataset
self.assertEqual(len(rows), len(dataset))
for idx, row in enumerate(rows):
data = row['data']
cmp = components[idx]
2022-02-17 01:10:48 +00:00
# Should have guessed the correct part
data['part'] = cmp.pk
2022-02-17 01:10:48 +00:00
# Check some specific error messages
self.assertEqual(rows[0]['data']['errors']['quantity'], 'Quantity must be greater than zero')
self.assertEqual(rows[5]['data']['errors']['part'], 'Part is not designated as a component')
def test_part_guess(self):
"""
Test part 'guessing' when PK values are not supplied
"""
dataset = tablib.Dataset()
# Should be able to 'guess' the part from the name
dataset.headers = ['part_name', 'quantity']
components = Part.objects.filter(component=True)
for idx, cmp in enumerate(components):
dataset.append([
f"Component {idx}",
10,
])
2022-02-17 02:07:16 +00:00
url = reverse('api-bom-import-extract')
response = self.post(
url,
{
'columns': dataset.headers,
'rows': [row for row in dataset],
},
expected_code=201,
)
rows = response.data['rows']
self.assertEqual(len(rows), 10)
for idx in range(10):
2022-02-17 02:07:16 +00:00
self.assertEqual(rows[idx]['data']['part'], components[idx].pk)
# Should also be able to 'guess' part by the IPN value
dataset = tablib.Dataset()
dataset.headers = ['part_ipn', 'quantity']
for idx, cmp in enumerate(components):
dataset.append([
f"CMP_{idx}",
10,
])
2022-02-17 02:07:16 +00:00
response = self.post(
url,
{
'columns': dataset.headers,
'rows': [row for row in dataset],
},
expected_code=201,
)
rows = response.data['rows']
self.assertEqual(len(rows), 10)
for idx in range(10):
2022-02-17 02:07:16 +00:00
self.assertEqual(rows[idx]['data']['part'], components[idx].pk)
def test_levels(self):
"""
Test that multi-level BOMs are correctly handled during upload
"""
2022-02-17 02:07:16 +00:00
url = reverse('api-bom-import-extract')
dataset = tablib.Dataset()
dataset.headers = ['level', 'part', 'quantity']
components = Part.objects.filter(component=True)
for idx, cmp in enumerate(components):
dataset.append([
idx % 3,
cmp.pk,
2,
])
2022-02-17 02:07:16 +00:00
response = self.post(
url,
{
'rows': [row for row in dataset],
'columns': dataset.headers,
},
expected_code=201,
)
2022-02-17 02:07:16 +00:00
rows = response.data['rows']
# Only parts at index 1, 4, 7 should have been returned
self.assertEqual(len(response.data['rows']), 3)
2022-02-17 02:07:16 +00:00
self.assertEqual(rows[0]['data']['part'], 3)
self.assertEqual(rows[1]['data']['part'], 6)
self.assertEqual(rows[2]['data']['part'], 9)