From fd9f25dc0dd50d59905e0a7ba85f6fd6f15f3d18 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 17 May 2022 21:26:33 +1000 Subject: [PATCH 01/12] Adds helper function for testing a file download via the api --- InvenTree/InvenTree/api_tester.py | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index c55c3d3ba3..0420e93369 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -2,6 +2,10 @@ Helper functions for performing API unit tests """ +import io +import re + +from django.http.response import StreamingHttpResponse from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from rest_framework.test import APITestCase @@ -165,3 +169,33 @@ class InvenTreeAPITestCase(APITestCase): self.assertEqual(response.status_code, expected_code) return response + + def download_file(self, url, data, expected_code=None, expected_fn=None): + """ + Download a file from the server, and return an in-memory file + """ + + response = self.client.get(url, data=data, format='json') + + if expected_code is not None: + self.assertEqual(response.status_code, expected_code) + + # Check that the response is of the correct type + if not isinstance(response, StreamingHttpResponse): + raise ValueError("Response is not a StreamingHttpResponse object as expected") + + # Extract filename + disposition = response.headers['Content-Disposition'] + + result = re.search(r'attachment; filename="([\w.]+)"', disposition) + + fn = result.groups()[0] + + with io.BytesIO() as fo: + fo.name = fn + fo.write(response.getvalue()) + + if expected_fn is not None: + self.assertEqual(expected_fn, fn) + + return fo \ No newline at end of file From 55000d5c483f9aded276652e50ad49bfafa50e3a Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 17 May 2022 22:28:46 +1000 Subject: [PATCH 02/12] Add ability to download file as StringIO or BytesIO --- InvenTree/InvenTree/api_tester.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 0420e93369..122ce1950f 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -170,7 +170,7 @@ class InvenTreeAPITestCase(APITestCase): return response - def download_file(self, url, data, expected_code=None, expected_fn=None): + def download_file(self, url, data, expected_code=None, expected_fn=None, decode=False): """ Download a file from the server, and return an in-memory file """ @@ -191,11 +191,20 @@ class InvenTreeAPITestCase(APITestCase): fn = result.groups()[0] - with io.BytesIO() as fo: - fo.name = fn - fo.write(response.getvalue()) - if expected_fn is not None: self.assertEqual(expected_fn, fn) - - return fo \ No newline at end of file + + if decode: + # Decode data and return as StringIO file object + fo = io.StringIO() + fo.name = fo + fo.write(response.getvalue().decode('UTF-8')) + else: + # Return a a BytesIO file object + fo = io.BytesIO() + fo.name = fn + fo.write(response.getvalue()) + + fo.seek(0) + + return fo From 920e7e0bb7dfafec5365d8ddbbdbbe8b482358b9 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 17 May 2022 23:21:30 +1000 Subject: [PATCH 03/12] Adds helper function to process and validate a downloaded .csv file --- InvenTree/InvenTree/api_tester.py | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 122ce1950f..3408322bd3 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -2,6 +2,7 @@ Helper functions for performing API unit tests """ +import csv import io import re @@ -208,3 +209,37 @@ class InvenTreeAPITestCase(APITestCase): fo.seek(0) return fo + + def process_csv(self, fo, delimiter=',', required_cols=None, excluded_cols=None, required_rows=None): + """ + Helper function to process and validate a downloaded csv file + """ + + # Check that the correct object type has been passed + self.assertTrue(isinstance(fo, io.StringIO)) + + fo.seek(0) + + reader = csv.reader(fo, delimiter=delimiter) + + headers = [] + rows = [] + + for idx, row in enumerate(reader): + if idx == 0: + headers = row + else: + rows.append(row) + + if required_cols is not None: + for col in required_cols: + self.assertIn(col, required_cols) + + if excluded_cols is not None: + for col in excluded_cols: + self.assertNotIn(col, excluded_cols) + + if required_rows is not None: + self.assertEqual(len(rows), required_rows) + + return (headers, rows) From a44267c306e5f3bec3dc9386022b8d2cd1fae735 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 17 May 2022 23:22:40 +1000 Subject: [PATCH 04/12] bug fix --- InvenTree/InvenTree/api_tester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 3408322bd3..4fb6a9e652 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -233,11 +233,11 @@ class InvenTreeAPITestCase(APITestCase): if required_cols is not None: for col in required_cols: - self.assertIn(col, required_cols) + self.assertIn(col, headers) if excluded_cols is not None: for col in excluded_cols: - self.assertNotIn(col, excluded_cols) + self.assertNotIn(col, headers) if required_rows is not None: self.assertEqual(len(rows), required_rows) From 3488da19b024b94f27ab8d9f71588fe00cf70eca Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 17 May 2022 23:47:09 +1000 Subject: [PATCH 05/12] Return data as a list of dict objects --- InvenTree/InvenTree/api_tester.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 4fb6a9e652..1fa2816653 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -242,4 +242,15 @@ class InvenTreeAPITestCase(APITestCase): if required_rows is not None: self.assertEqual(len(rows), required_rows) - return (headers, rows) + # Return the file data as a list of dict items, based on the headers + data = [] + + for row in rows: + entry = {} + + for idx, col in enumerate(headers): + entry[col] = row[idx] + + data.append(entry) + + return data From a6be0b32c6a660c83698c476d0659b991212528e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 17 May 2022 23:48:58 +1000 Subject: [PATCH 06/12] Add unit tests for exporting SalesOrder data --- InvenTree/InvenTree/api_tester.py | 12 ++-- InvenTree/order/admin.py | 6 ++ InvenTree/order/api.py | 4 +- InvenTree/order/test_api.py | 93 +++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 1fa2816653..9dde7c0f52 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -184,7 +184,7 @@ class InvenTreeAPITestCase(APITestCase): # Check that the response is of the correct type if not isinstance(response, StreamingHttpResponse): raise ValueError("Response is not a StreamingHttpResponse object as expected") - + # Extract filename disposition = response.headers['Content-Disposition'] @@ -230,24 +230,24 @@ class InvenTreeAPITestCase(APITestCase): headers = row else: rows.append(row) - + if required_cols is not None: for col in required_cols: self.assertIn(col, headers) - + if excluded_cols is not None: for col in excluded_cols: self.assertNotIn(col, headers) - + if required_rows is not None: self.assertEqual(len(rows), required_rows) - + # Return the file data as a list of dict items, based on the headers data = [] for row in rows: entry = {} - + for idx, col in enumerate(headers): entry[col] = row[idx] diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index eaeebff04d..ac74a004e3 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -105,6 +105,9 @@ class PurchaseOrderResource(ModelResource): model = PurchaseOrder skip_unchanged = True clean_model_instances = True + exclude = [ + 'metadata', + ] class PurchaseOrderLineItemResource(ModelResource): @@ -147,6 +150,9 @@ class SalesOrderResource(ModelResource): model = SalesOrder skip_unchanged = True clean_model_instances = True + exclude = [ + 'metadata', + ] class SalesOrderLineItemResource(ModelResource): diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index e65463d55c..2a9c3b99d2 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -667,9 +667,9 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView): outstanding = str2bool(outstanding) if outstanding: - queryset = queryset.filter(status__in=models.SalesOrderStatus.OPEN) + queryset = queryset.filter(status__in=SalesOrderStatus.OPEN) else: - queryset = queryset.exclude(status__in=models.SalesOrderStatus.OPEN) + queryset = queryset.exclude(status__in=SalesOrderStatus.OPEN) # Filter by 'overdue' status overdue = params.get('overdue', None) diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 76aa8670a4..1a044c9b46 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -2,6 +2,8 @@ Tests for the Order API """ +import io + from datetime import datetime, timedelta from rest_framework import status @@ -908,6 +910,97 @@ class SalesOrderTest(OrderTest): self.assertEqual(order.get_metadata('xyz'), 'abc') +class SalesOrderDownloadTest(OrderTest): + """Unit tests for downloading SalesOrder data via the API endpoint""" + + def test_download_fail(self): + """Test that downloading without the 'export' option fails""" + + url = reverse('api-so-list') + + with self.assertRaises(ValueError): + self.download_file(url, {}, expected_code=200) + + def test_download_xls(self): + url = reverse('api-so-list') + + # Download .xls file + fo = self.download_file( + url, + { + 'export': 'xls', + }, + expected_code=200, + expected_fn='InvenTree_SalesOrders.xls', + ) + + self.assertTrue(isinstance(fo, io.BytesIO)) + + def test_download_csv(self): + + url = reverse('api-so-list') + + required_cols = [ + 'line_items', + 'id', + 'reference', + 'customer', + 'status', + 'shipment_date', + 'notes', + 'description', + ] + + excluded_cols = [ + 'metadata' + ] + + # Download .xls file + with self.download_file( + url, + { + 'export': 'csv', + }, + expected_code=200, + expected_fn='InvenTree_SalesOrders.csv', + decode=True + ) as fo: + + data = self.process_csv( + fo, + required_cols=required_cols, + excluded_cols=excluded_cols, + required_rows=models.SalesOrder.objects.count() + ) + + for line in data: + + order = models.SalesOrder.objects.get(pk=line['id']) + + self.assertEqual(line['description'], order.description) + self.assertEqual(line['status'], str(order.status)) + + # Download only outstanding sales orders + with self.download_file( + url, + { + 'export': 'tsv', + 'outstanding': True, + }, + expected_code=200, + expected_fn='InvenTree_SalesOrders.tsv', + decode=True, + ) as fo: + + self.process_csv( + fo, + required_cols=required_cols, + excluded_cols=excluded_cols, + required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatus.OPEN).count(), + delimiter='\t', + ) + + class SalesOrderAllocateTest(OrderTest): """ Unit tests for allocating stock items against a SalesOrder From 0d078768feeccc087b9deaefe016d1d7684b1d21 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 18 May 2022 00:08:11 +1000 Subject: [PATCH 07/12] Unit tests for downloading PurchaseOrder data --- InvenTree/InvenTree/api_tester.py | 2 +- InvenTree/order/test_api.py | 63 +++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 9dde7c0f52..34976ffbfe 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -171,7 +171,7 @@ class InvenTreeAPITestCase(APITestCase): return response - def download_file(self, url, data, expected_code=None, expected_fn=None, decode=False): + def download_file(self, url, data, expected_code=None, expected_fn=None, decode=True): """ Download a file from the server, and return an in-memory file """ diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 1a044c9b46..fffeb29216 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -325,6 +325,62 @@ class PurchaseOrderTest(OrderTest): self.assertEqual(order.get_metadata('yam'), 'yum') +class PurchaseOrderDownloadTest(OrderTest): + """Unit tests for downloading PurchaseOrder data via the API endpoint""" + + required_cols = [ + 'id', + 'line_items', + 'description', + 'issue_date', + 'notes', + 'reference', + 'status', + 'supplier_reference', + ] + + excluded_cols = [ + 'metadata', + ] + + def test_download_wrong_format(self): + """Incorrect format should default raise an error""" + + url = reverse('api-po-list') + + with self.assertRaises(ValueError): + self.download_file( + url, + { + 'export': 'xyz', + } + ) + + def test_download_csv(self): + """Download PurchaseOrder data as .csv""" + + with self.download_file( + reverse('api-po-list'), + { + 'export': 'csv', + }, + expected_code=200, + expected_fn='InvenTree_PurchaseOrders.csv', + ) as fo: + + data = self.process_csv( + fo, + required_cols=self.required_cols, + excluded_cols=self.excluded_cols, + required_rows=models.PurchaseOrder.objects.count() + ) + + for row in data: + order = models.PurchaseOrder.objects.get(pk=row['id']) + + self.assertEqual(order.description, row['description']) + self.assertEqual(order.reference, row['reference']) + class PurchaseOrderReceiveTest(OrderTest): """ Unit tests for receiving items against a PurchaseOrder @@ -925,16 +981,15 @@ class SalesOrderDownloadTest(OrderTest): url = reverse('api-so-list') # Download .xls file - fo = self.download_file( + with self.download_file( url, { 'export': 'xls', }, expected_code=200, expected_fn='InvenTree_SalesOrders.xls', - ) - - self.assertTrue(isinstance(fo, io.BytesIO)) + ) as fo: + self.assertTrue(isinstance(fo, io.BytesIO)) def test_download_csv(self): From c5b14944a1a710d0afbac5d7d5cc852ad6569fd7 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 18 May 2022 00:31:43 +1000 Subject: [PATCH 08/12] Unit tests for downloading BuildOrder data --- InvenTree/build/admin.py | 3 ++- InvenTree/build/test_api.py | 44 +++++++++++++++++++++++++++++++++++++ InvenTree/order/test_api.py | 1 + 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index 32d843d057..5988850fe4 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -16,7 +16,7 @@ class BuildResource(ModelResource): # but we don't for other ones. # TODO: 2022-05-12 - Need to investigate why this is the case! - pk = Field(attribute='pk') + id = Field(attribute='pk') reference = Field(attribute='reference') @@ -45,6 +45,7 @@ class BuildResource(ModelResource): clean_model_instances = True exclude = [ 'lft', 'rght', 'tree_id', 'level', + 'metadata', ] diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index 068493ad5e..4ba54e9c73 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -511,6 +511,50 @@ class BuildTest(BuildAPITest): self.assertIn('This build output has already been completed', str(response.data)) + def test_download_build_orders(self): + + required_cols = [ + 'reference', + 'status', + 'completed', + 'batch', + 'notes', + 'title', + 'part', + 'part_name', + 'id', + 'quantity', + ] + + excluded_cols = [ + 'lft', 'rght', 'tree_id', 'level', + 'metadata', + ] + + with self.download_file( + reverse('api-build-list'), + { + 'export': 'csv', + } + ) as fo: + + data = self.process_csv( + fo, + required_cols=required_cols, + excluded_cols=excluded_cols, + required_rows=Build.objects.count() + ) + + for row in data: + + build = Build.objects.get(pk=row['id']) + + self.assertEqual(str(build.part.pk), row['part']) + self.assertEqual(build.part.full_name, row['part_name']) + + self.assertEqual(build.reference, row['reference']) + self.assertEqual(build.title, row['title']) + class BuildAllocationTest(BuildAPITest): """ diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index fffeb29216..bb058d84bb 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -381,6 +381,7 @@ class PurchaseOrderDownloadTest(OrderTest): self.assertEqual(order.description, row['description']) self.assertEqual(order.reference, row['reference']) + class PurchaseOrderReceiveTest(OrderTest): """ Unit tests for receiving items against a PurchaseOrder From 1c6d79451ecfee7c3fc1d53ccb4e0c8bdf404dec Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 18 May 2022 07:25:43 +1000 Subject: [PATCH 09/12] Don't decode downloaded .xls file --- InvenTree/order/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index bb058d84bb..04be484310 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -989,6 +989,7 @@ class SalesOrderDownloadTest(OrderTest): }, expected_code=200, expected_fn='InvenTree_SalesOrders.xls', + deode=False, ) as fo: self.assertTrue(isinstance(fo, io.BytesIO)) From b6c2ade940c814d381c430bab71d840c327d997e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 18 May 2022 07:52:29 +1000 Subject: [PATCH 10/12] Add unit test for downloading Part data --- InvenTree/order/test_api.py | 2 +- InvenTree/part/admin.py | 2 ++ InvenTree/part/test_api.py | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 04be484310..f06e26370d 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -989,7 +989,7 @@ class SalesOrderDownloadTest(OrderTest): }, expected_code=200, expected_fn='InvenTree_SalesOrders.xls', - deode=False, + decode=False, ) as fo: self.assertTrue(isinstance(fo, io.BytesIO)) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 8021bd10fa..5fafc37fea 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -45,6 +45,7 @@ class PartResource(ModelResource): exclude = [ 'bom_checksum', 'bom_checked_by', 'bom_checked_date', 'lft', 'rght', 'tree_id', 'level', + 'metadata', ] def get_queryset(self): @@ -98,6 +99,7 @@ class PartCategoryResource(ModelResource): exclude = [ # Exclude MPTT internal model fields 'lft', 'rght', 'tree_id', 'level', + 'metadata', ] def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 28df4503c9..46b7a477f8 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -822,6 +822,58 @@ class PartAPITest(InvenTreeAPITestCase): response = self.get('/api/part/10004/', {}) self.assertEqual(response.data['variant_stock'], 500) + def test_part_download(self): + """Test download of part data via the API""" + + url = reverse('api-part-list') + + required_cols = [ + 'id', + 'name', + 'description', + 'in_stock', + 'category_name', + 'keywords', + 'is_template', + 'virtual', + 'trackable', + 'active', + 'notes', + 'creation_date', + ] + + excluded_cols = [ + 'lft', 'rght', 'level', 'tree_id', + 'metadata', + ] + + with self.download_file( + url, + { + 'export': 'csv', + }, + expected_fn='InvenTree_Parts.csv', + ) as fo: + + data = self.process_csv( + fo, + excluded_cols=excluded_cols, + required_cols=required_cols, + required_rows=Part.objects.count(), + ) + + for row in data: + part = Part.objects.get(pk=row['id']) + + if part.IPN: + self.assertEqual(part.IPN, row['IPN']) + + self.assertEqual(part.name, row['name']) + self.assertEqual(part.description, row['description']) + + if part.category: + self.assertEqual(part.category.name, row['category_name']) + class PartDetailTests(InvenTreeAPITestCase): """ From aa3a86f3728d69aa3132be14513756d3b729fa3b Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 18 May 2022 21:33:40 +1000 Subject: [PATCH 11/12] Exclude metadata from StockLocation and StockItem model resource class --- InvenTree/stock/admin.py | 3 ++- InvenTree/stock/test_api.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 3d931e2d7c..4b7cf38bf5 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -31,6 +31,7 @@ class LocationResource(ModelResource): exclude = [ # Exclude MPTT internal model fields 'lft', 'rght', 'tree_id', 'level', + 'metadata', ] def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): @@ -119,7 +120,7 @@ class StockItemResource(ModelResource): # Exclude MPTT internal model fields 'lft', 'rght', 'tree_id', 'level', # Exclude internal fields - 'serial_int', + 'serial_int', 'metadata', ] diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index 28a8e0de0b..ccdac8d2c6 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -344,6 +344,13 @@ class StockItemListTest(StockAPITestCase): for h in headers: self.assertIn(h, dataset.headers) + excluded_headers = [ + 'metadata', + ] + + for h in excluded_headers: + self.assertNotIn(h, dataset.headers) + # Now, add a filter to the results dataset = self.export_data({'location': 1}) From 0831b85e29cb94f892f2420cec7b7da20282e86e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 19 May 2022 01:39:16 +1000 Subject: [PATCH 12/12] Adding some unit tests for SalesOrderLineItem API --- InvenTree/order/test_api.py | 94 +++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index f06e26370d..6549b1d89d 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -381,6 +381,20 @@ class PurchaseOrderDownloadTest(OrderTest): self.assertEqual(order.description, row['description']) self.assertEqual(order.reference, row['reference']) + def test_download_line_items(self): + + with self.download_file( + reverse('api-po-line-list'), + { + 'export': 'xlsx', + }, + decode=False, + expected_code=200, + expected_fn='InvenTree_PurchaseOrderItems.xlsx', + ) as fo: + + self.assertTrue(isinstance(fo, io.BytesIO)) + class PurchaseOrderReceiveTest(OrderTest): """ @@ -967,6 +981,86 @@ class SalesOrderTest(OrderTest): self.assertEqual(order.get_metadata('xyz'), 'abc') +class SalesOrderLineItemTest(OrderTest): + """ + Tests for the SalesOrderLineItem API + """ + + def setUp(self): + + super().setUp() + + # List of salable parts + parts = Part.objects.filter(salable=True) + + # Create a bunch of SalesOrderLineItems for each order + for idx, so in enumerate(models.SalesOrder.objects.all()): + + for part in parts: + models.SalesOrderLineItem.objects.create( + order=so, + part=part, + quantity=(idx + 1) * 5, + reference=f"Order {so.reference} - line {idx}", + ) + + self.url = reverse('api-so-line-list') + + def test_so_line_list(self): + + # List *all* lines + + response = self.get( + self.url, + {}, + expected_code=200, + ) + + n = models.SalesOrderLineItem.objects.count() + + # We should have received *all* lines + self.assertEqual(len(response.data), n) + + # List *all* lines, but paginate + response = self.get( + self.url, + { + "limit": 5, + }, + expected_code=200, + ) + + self.assertEqual(response.data['count'], n) + self.assertEqual(len(response.data['results']), 5) + + n_orders = models.SalesOrder.objects.count() + n_parts = Part.objects.filter(salable=True).count() + + # List by part + for part in Part.objects.filter(salable=True): + response = self.get( + self.url, + { + 'part': part.pk, + 'limit': 10, + } + ) + + self.assertEqual(response.data['count'], n_orders) + + # List by order + for order in models.SalesOrder.objects.all(): + response = self.get( + self.url, + { + 'order': order.pk, + 'limit': 10, + } + ) + + self.assertEqual(response.data['count'], n_parts) + + class SalesOrderDownloadTest(OrderTest): """Unit tests for downloading SalesOrder data via the API endpoint"""