From 495798dc98e5f3154db4b8e4b5f7cb2c664ee9ed Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 15 May 2022 23:20:12 +1000 Subject: [PATCH 01/72] Install libwebp-dev as part of dockerfile --- docker/Dockerfile | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index cefd2c2b61..4c6a351adc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -62,7 +62,7 @@ RUN apk -U upgrade RUN apk add --no-cache git make bash \ gcc libgcc g++ libstdc++ \ gnupg \ - libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \ + libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev libwebp-dev \ libffi libffi-dev \ zlib zlib-dev \ # Special deps for WeasyPrint (these will be deprecated once WeasyPrint drops cairo requirement) diff --git a/requirements.txt b/requirements.txt index 5065b4f877..2369b18b44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ importlib_metadata # Backport for importlib.metadata inventree # Install the latest version of the InvenTree API python library markdown==3.3.4 # Force particular version of markdown pep8-naming==0.11.1 # PEP naming convention extension -pillow==9.0.1 # Image manipulation +pillow==9.1.0 # Image manipulation py-moneyed==0.8.0 # Specific version requirement for py-moneyed pygments==2.7.4 # Syntax highlighting python-barcode[images]==0.13.1 # Barcode generator From 55f87033b25fd9e684c1d53ba281b09f1cc5ccf9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 15 May 2022 23:36:41 +1000 Subject: [PATCH 02/72] Add unit tests for .webp support --- InvenTree/part/test_api.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index f0770eb1f5..4e4162341a 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -970,24 +970,29 @@ class PartDetailTests(InvenTreeAPITestCase): ) self.assertEqual(response.status_code, 400) + self.assertIn('Upload a valid image', str(response.data)) - # Now try to upload a valid image file - img = PIL.Image.new('RGB', (128, 128), color='red') - img.save('dummy_image.jpg') + # Now try to upload a valid image file, in multiple formats + for fmt in ['jpg', 'png', 'bmp', 'webp']: + fn = f'dummy_image.{fmt}' - with open('dummy_image.jpg', 'rb') as dummy_image: - response = upload_client.patch( - url, - { - 'image': dummy_image, - }, - format='multipart', - ) + img = PIL.Image.new('RGB', (128, 128), color='red') + img.save(fn) - self.assertEqual(response.status_code, 200) + with open(fn, 'rb') as dummy_image: + response = upload_client.patch( + url, + { + 'image': dummy_image, + }, + format='multipart', + ) - # And now check that the image has been set - p = Part.objects.get(pk=pk) + self.assertEqual(response.status_code, 200) + + # And now check that the image has been set + p = Part.objects.get(pk=pk) + self.assertIsNotNone(p.image) def test_details(self): """ From 47269a88d2d04dc2457e5b6bb7f17c3868595640 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 15 May 2022 23:37:01 +1000 Subject: [PATCH 03/72] Ensure unit tests are run within a docker context as part of CI builds --- .github/workflows/docker_test.yaml | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml index d96621ee66..69fbb48fe7 100644 --- a/.github/workflows/docker_test.yaml +++ b/.github/workflows/docker_test.yaml @@ -3,9 +3,8 @@ # This CI action runs on pushes to either the master or stable branches # 1. Build the development docker image (as per the documentation) -# 2. Install requied python libs into the docker container -# 3. Launch the container -# 4. Check that the API endpoint is available +# 2. Launch the development server, and update the installation +# 3. Run unit tests within the docker context name: Docker Test @@ -15,6 +14,10 @@ on: - 'master' - 'stable' + pull_request: + branches-ignore: + - l10* + jobs: docker: @@ -26,12 +29,14 @@ jobs: - name: Build Docker Image run: | cd docker - docker-compose -f docker-compose.sqlite.yml build - docker-compose -f docker-compose.sqlite.yml run inventree-dev-server invoke update - docker-compose -f docker-compose.sqlite.yml up -d - - name: Sleepy Time - run: sleep 60 - - name: Test API + docker-compose build + docker-compose run inventree-dev-server invoke update + docker-compose up -d + - name: Wait for Server run: | - pip install requests - python3 ci/check_api_endpoint.py + cd docker + docker-compose run inventree-dev-server invoke wait + - name: Run unit tests + run: | + cd docker + docker-compose run inventree-dev-server invoke test From 206da0232867c8f3b0574649012c8d4337640e4b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 16 May 2022 00:21:05 +1000 Subject: [PATCH 04/72] Skip some git hash checks if running tests under docker --- InvenTree/part/test_part.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 5932c36757..2241b68c0d 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from allauth.account.models import EmailAddress +from django.conf import settings from django.contrib.auth import get_user_model from django.test import TestCase @@ -67,11 +68,21 @@ class TemplateTagTest(TestCase): def test_hash(self): result_hash = inventree_extras.inventree_commit_hash() - self.assertGreater(len(result_hash), 5) + if settings.DOCKER: + # Testing inside docker environment *may* return an empty git commit hash + # In such a case, skip this check + pass + else: + self.assertGreater(len(result_hash), 5) def test_date(self): d = inventree_extras.inventree_commit_date() - self.assertEqual(len(d.split('-')), 3) + if settings.DOCKER: + # Testing inside docker environment *may* return an empty git commit hash + # In such a case, skip this check + pass + else: + self.assertEqual(len(d.split('-')), 3) def test_github(self): self.assertIn('github.com', inventree_extras.inventree_github_url()) From 7e6d3d81b99e9d741056a3f22dba50df20d08de4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 16 May 2022 21:47:09 +1000 Subject: [PATCH 05/72] Update dockerfile to 3.14 --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4c6a351adc..1b7c16db30 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.13 as base +FROM alpine:3.14 as base # GitHub source ARG repository="https://github.com/inventree/InvenTree.git" @@ -68,7 +68,7 @@ RUN apk add --no-cache git make bash \ # Special deps for WeasyPrint (these will be deprecated once WeasyPrint drops cairo requirement) cairo cairo-dev pango pango-dev gdk-pixbuf \ # Fonts - fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto \ + fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans font-croscore font-noto \ # Core python python3 python3-dev py3-pip \ # SQLite support From b630fb285615260b660cefbd3b879d1c74436155 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 May 2022 16:41:00 +0200 Subject: [PATCH 06/72] update envguard import --- InvenTree/InvenTree/tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index dc3aff85e6..97bd77a1e1 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -1,5 +1,6 @@ import json -from test.support import EnvironmentVarGuard + +from test import support from django.test import TestCase, override_settings import django.core.exceptions as django_exceptions @@ -449,7 +450,7 @@ class TestSettings(TestCase): def setUp(self) -> None: self.user_mdl = get_user_model() - self.env = EnvironmentVarGuard() + self.env = support.EnvironmentVarGuard() def run_reload(self): from plugin import registry From 825c50a43808b62b5bdd13ee3ca57dc8688859e8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 08:40:43 +1000 Subject: [PATCH 07/72] Change import style --- InvenTree/InvenTree/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index dc3aff85e6..5ec8a863bc 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -1,5 +1,5 @@ import json -from test.support import EnvironmentVarGuard +from test import support from django.test import TestCase, override_settings import django.core.exceptions as django_exceptions @@ -449,7 +449,7 @@ class TestSettings(TestCase): def setUp(self) -> None: self.user_mdl = get_user_model() - self.env = EnvironmentVarGuard() + self.env = support.EnvironmentVarGuard() def run_reload(self): from plugin import registry From fd9f25dc0dd50d59905e0a7ba85f6fd6f15f3d18 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 21:26:33 +1000 Subject: [PATCH 08/72] 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 Date: Tue, 17 May 2022 22:28:46 +1000 Subject: [PATCH 09/72] 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 Date: Tue, 17 May 2022 23:21:30 +1000 Subject: [PATCH 10/72] 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 Date: Tue, 17 May 2022 23:22:40 +1000 Subject: [PATCH 11/72] 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 Date: Tue, 17 May 2022 23:47:09 +1000 Subject: [PATCH 12/72] 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 Date: Tue, 17 May 2022 23:48:58 +1000 Subject: [PATCH 13/72] 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 Date: Wed, 18 May 2022 00:08:11 +1000 Subject: [PATCH 14/72] 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 Date: Wed, 18 May 2022 00:31:43 +1000 Subject: [PATCH 15/72] 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 a40f189c7a6e2687306f15648e0d34d17a2bbfcd Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 17 May 2022 19:23:50 +0200 Subject: [PATCH 16/72] Use unierest mock for env setting --- InvenTree/InvenTree/tests.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 97bd77a1e1..e1e2900d1c 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -1,6 +1,6 @@ import json -from test import support +from unittest import mock from django.test import TestCase, override_settings import django.core.exceptions as django_exceptions @@ -450,12 +450,11 @@ class TestSettings(TestCase): def setUp(self) -> None: self.user_mdl = get_user_model() - self.env = support.EnvironmentVarGuard() - def run_reload(self): + def run_reload(self, envs): from plugin import registry - with self.env: + with mock.patch.dict(os.environ, envs): settings.USER_ADDED = False registry.reload_plugins() @@ -471,15 +470,17 @@ class TestSettings(TestCase): self.assertEqual(user_count(), 0) # not enough set - self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - self.run_reload() + envs = {} + envs['INVENTREE_ADMIN_USER'] = 'admin' + self.run_reload(envs) self.assertEqual(user_count(), 0) # enough set - self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email - self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password - self.run_reload() + envs = {'INVENTREE_ADMIN_USER': 'admin', # set username + 'INVENTREE_ADMIN_EMAIL': 'info@example.com', # set email + 'INVENTREE_ADMIN_PASSWORD': 'password123' # set password + } + self.run_reload(envs) self.assertEqual(user_count(), 1) # make sure to clean up From 1c6d79451ecfee7c3fc1d53ccb4e0c8bdf404dec Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 07:25:43 +1000 Subject: [PATCH 17/72] 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 Date: Wed, 18 May 2022 07:52:29 +1000 Subject: [PATCH 18/72] 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 6e19187929fa19334c571f43be76528e9719792b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:01:25 +0200 Subject: [PATCH 19/72] add missing import --- InvenTree/InvenTree/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 08579b29a7..c569310049 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -1,4 +1,5 @@ import json +import os from unittest import mock From 9f0b00cc0eaace16a9ca609168c8aa94d102c7ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:01:52 +0200 Subject: [PATCH 20/72] replace old function --- InvenTree/InvenTree/tests.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index c569310049..09bbe569ce 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -514,8 +514,7 @@ class TestSettings(TestCase): self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file()) # with env set - with self.env: - self.env.set('INVENTREE_CONFIG_FILE', 'my_special_conf.yaml') + with mock.patch.dict(os.environ, {'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}): self.assertIn('InvenTree/InvenTree/my_special_conf.yaml', config.get_config_file()) def test_helpers_plugin_file(self): @@ -523,8 +522,7 @@ class TestSettings(TestCase): self.assertIn('InvenTree/InvenTree/plugins.txt', config.get_plugin_file()) # with env set - with self.env: - self.env.set('INVENTREE_PLUGIN_FILE', 'my_special_plugins.txt') + with mock.patch.dict(os.environ, {'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}): self.assertIn('my_special_plugins.txt', config.get_plugin_file()) def test_helpers_setting(self): @@ -533,8 +531,7 @@ class TestSettings(TestCase): self.assertEqual(config.get_setting(TEST_ENV_NAME, None, '123!'), '123!') # with env set - with self.env: - self.env.set(TEST_ENV_NAME, '321') + with mock.patch.dict(os.environ, {'TEST_ENV_NAME': '321'}): self.assertEqual(config.get_setting(TEST_ENV_NAME, None), '321') From ca7fb691acc04a07d5b28aa4f7cec1a924a46953 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:02:14 +0200 Subject: [PATCH 21/72] make change patch simpler --- InvenTree/InvenTree/tests.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 09bbe569ce..6e7b9c5444 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -476,18 +476,17 @@ class TestSettings(TestCase): self.assertEqual(user_count(), 1) # not enough set - envs = {} - envs['INVENTREE_ADMIN_USER'] = 'admin' - self.run_reload(envs) + self.run_reload({ + 'INVENTREE_ADMIN_USER': 'admin' + }) self.assertEqual(user_count(), 0) # enough set - envs = { + self.run_reload({ 'INVENTREE_ADMIN_USER': 'admin', # set username 'INVENTREE_ADMIN_EMAIL': 'info@example.com', # set email 'INVENTREE_ADMIN_PASSWORD': 'password123' # set password - } - self.run_reload(envs) + }) self.assertEqual(user_count(), 1) # make sure to clean up From a570dab5e5ada31529889abfd6b74113757e46dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:04:15 +0200 Subject: [PATCH 22/72] generalise function to make new methods simpler --- InvenTree/InvenTree/tests.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 6e7b9c5444..02c95192df 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -457,10 +457,14 @@ class TestSettings(TestCase): self.user = user.objects.create_superuser('testuser1', 'test1@testing.com', 'password1') self.client.login(username='testuser1', password='password1') + def in_env_context(self, envs={}): + """Patch the env to include the given dict""" + return mock.patch.dict(os.environ, envs) + def run_reload(self, envs): from plugin import registry - with mock.patch.dict(os.environ, envs): + with self.in_env_context(envs): settings.USER_ADDED = False registry.reload_plugins() @@ -513,7 +517,7 @@ class TestSettings(TestCase): self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file()) # with env set - with mock.patch.dict(os.environ, {'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}): + with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}): self.assertIn('InvenTree/InvenTree/my_special_conf.yaml', config.get_config_file()) def test_helpers_plugin_file(self): @@ -521,7 +525,7 @@ class TestSettings(TestCase): self.assertIn('InvenTree/InvenTree/plugins.txt', config.get_plugin_file()) # with env set - with mock.patch.dict(os.environ, {'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}): + with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}): self.assertIn('my_special_plugins.txt', config.get_plugin_file()) def test_helpers_setting(self): @@ -530,7 +534,7 @@ class TestSettings(TestCase): self.assertEqual(config.get_setting(TEST_ENV_NAME, None, '123!'), '123!') # with env set - with mock.patch.dict(os.environ, {'TEST_ENV_NAME': '321'}): + with self.in_env_context({'TEST_ENV_NAME': '321'}): self.assertEqual(config.get_setting(TEST_ENV_NAME, None), '321') From 9b377608568df1fa9b81a47d33127c43d670f2cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:06:00 +0200 Subject: [PATCH 23/72] fix assertations --- InvenTree/InvenTree/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 02c95192df..f56e9809d0 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -483,7 +483,7 @@ class TestSettings(TestCase): self.run_reload({ 'INVENTREE_ADMIN_USER': 'admin' }) - self.assertEqual(user_count(), 0) + self.assertEqual(user_count(), 1) # enough set self.run_reload({ @@ -491,7 +491,7 @@ class TestSettings(TestCase): 'INVENTREE_ADMIN_EMAIL': 'info@example.com', # set email 'INVENTREE_ADMIN_PASSWORD': 'password123' # set password }) - self.assertEqual(user_count(), 1) + self.assertEqual(user_count(), 2) # make sure to clean up settings.TESTING_ENV = False From 4ac7d9626c176493e5c8b4baec5a473609924e4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:07:28 +0200 Subject: [PATCH 24/72] add missing test from merge back in --- InvenTree/InvenTree/tests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index f56e9809d0..f053a7c27b 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -493,6 +493,17 @@ class TestSettings(TestCase): }) self.assertEqual(user_count(), 2) + # create user manually + self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password') + self.assertEqual(user_count(), 3) + # check it will not be created again + self.run_reload({ + 'INVENTREE_ADMIN_USER': 'testuser', + 'INVENTREE_ADMIN_EMAIL': 'test@testing.com', + 'INVENTREE_ADMIN_PASSWORD': 'password', + }) + self.assertEqual(user_count(), 3) + # make sure to clean up settings.TESTING_ENV = False From bdf28b72df19fa7b7fa1ddc94977b8438aaf12d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:25:44 +0200 Subject: [PATCH 25/72] fix default --- InvenTree/InvenTree/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index f053a7c27b..bad14f8e00 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -461,7 +461,7 @@ class TestSettings(TestCase): """Patch the env to include the given dict""" return mock.patch.dict(os.environ, envs) - def run_reload(self, envs): + def run_reload(self, envs={}): from plugin import registry with self.in_env_context(envs): From 9a0189b6bbfb2a8feda0191a05b0d4da77c71319 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 May 2022 02:30:07 +0200 Subject: [PATCH 26/72] fix env name --- InvenTree/InvenTree/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index bad14f8e00..1573a4387a 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -545,7 +545,7 @@ class TestSettings(TestCase): self.assertEqual(config.get_setting(TEST_ENV_NAME, None, '123!'), '123!') # with env set - with self.in_env_context({'TEST_ENV_NAME': '321'}): + with self.in_env_context({TEST_ENV_NAME: '321'}): self.assertEqual(config.get_setting(TEST_ENV_NAME, None), '321') From c4208782c5ef8171ae102e4254e77f08df41a7cf Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 18 May 2022 02:31:04 +0200 Subject: [PATCH 27/72] Update docker_test.yaml --- .github/workflows/docker_test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml index 69fbb48fe7..9b38743ab7 100644 --- a/.github/workflows/docker_test.yaml +++ b/.github/workflows/docker_test.yaml @@ -13,6 +13,7 @@ on: branches: - 'master' - 'stable' + - 'webp-support' pull_request: branches-ignore: From 21750c92d399af82aa738d639ea7c48939a32dc7 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 18 May 2022 02:33:09 +0200 Subject: [PATCH 28/72] remove branch from test --- .github/workflows/docker_test.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml index 9b38743ab7..69fbb48fe7 100644 --- a/.github/workflows/docker_test.yaml +++ b/.github/workflows/docker_test.yaml @@ -13,7 +13,6 @@ on: branches: - 'master' - 'stable' - - 'webp-support' pull_request: branches-ignore: From 3b53260d751e0ca239e4daa02eefaea167c82c33 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 11:51:14 +1000 Subject: [PATCH 29/72] Allow some variation in unit test --- InvenTree/InvenTree/tests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 1573a4387a..9ddde26418 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -533,7 +533,13 @@ class TestSettings(TestCase): def test_helpers_plugin_file(self): # normal run - not configured - self.assertIn('InvenTree/InvenTree/plugins.txt', config.get_plugin_file()) + + valid = [ + 'inventree/plugins.txt', + 'inventree/dev/plugins.txt', + ] + + self.assertIn(config.get_plugin_file().lower(), valid) # with env set with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}): From e57087de638ccf26851a229946c3727616c4a017 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 12:19:10 +1000 Subject: [PATCH 30/72] Fix unit test --- InvenTree/InvenTree/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 9ddde26418..f5f302b917 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -539,7 +539,7 @@ class TestSettings(TestCase): 'inventree/dev/plugins.txt', ] - self.assertIn(config.get_plugin_file().lower(), valid) + self.assertTrue(any([opt in config.get_plugin_file().lower() for opt in valid])) # with env set with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}): From 0f1dd3fe65043a6715ea804a7115e6091ffca4b2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 13:02:23 +1000 Subject: [PATCH 31/72] Same fix for config file test --- InvenTree/InvenTree/tests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index f5f302b917..5bb6a4aae2 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -525,7 +525,13 @@ class TestSettings(TestCase): def test_helpers_cfg_file(self): # normal run - not configured - self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file()) + + valid = [ + 'inventree/config.yaml', + 'inventree/dev/config.yaml', + ] + + self.assertTrue(any([opt in config.get_config_file().lower() for opt in valid])) # with env set with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}): From 810671f42383b9860e085bf2811426244d0f3214 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 13:40:57 +1000 Subject: [PATCH 32/72] Yet another fix --- InvenTree/InvenTree/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 5bb6a4aae2..501eed0834 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -535,7 +535,7 @@ class TestSettings(TestCase): # with env set with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}): - self.assertIn('InvenTree/InvenTree/my_special_conf.yaml', config.get_config_file()) + self.assertIn('inventree/inventree/my_special_conf.yaml', config.get_config_file().lower()) def test_helpers_plugin_file(self): # normal run - not configured From f53c8865ad742d7f2b64a2edc35632426c099cbb Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 14:14:40 +1000 Subject: [PATCH 33/72] Only run docker build on push --- .github/workflows/docker_test.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml index 69fbb48fe7..b4bb37715d 100644 --- a/.github/workflows/docker_test.yaml +++ b/.github/workflows/docker_test.yaml @@ -14,10 +14,6 @@ on: - 'master' - 'stable' - pull_request: - branches-ignore: - - l10* - jobs: docker: From ea3133be1d4da530941231e8aed5ba25e5a69ccb Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 15:11:37 +1000 Subject: [PATCH 34/72] Combine docker-build and docker-test CI steps - We are building anyway, may as well test --- .github/workflows/docker_latest.yaml | 14 ++++++++++ .github/workflows/docker_test.yaml | 38 ---------------------------- 2 files changed, 14 insertions(+), 38 deletions(-) delete mode 100644 .github/workflows/docker_test.yaml diff --git a/.github/workflows/docker_latest.yaml b/.github/workflows/docker_latest.yaml index 6b248fe0b9..9942407c07 100644 --- a/.github/workflows/docker_latest.yaml +++ b/.github/workflows/docker_latest.yaml @@ -18,6 +18,20 @@ jobs: - name: Check version number run: | python3 ci/check_version_number.py --dev + - name: Build Docker Image + run: | + cd docker + docker-compose build + docker-compose run inventree-dev-server invoke update + docker-compose up -d + - name: Wait for Server + run: | + cd docker + docker-compose run inventree-dev-server invoke wait + - name: Run unit tests + run: | + cd docker + docker-compose run inventree-dev-server invoke test - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml deleted file mode 100644 index b4bb37715d..0000000000 --- a/.github/workflows/docker_test.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# Test that the InvenTree docker image compiles correctly - -# This CI action runs on pushes to either the master or stable branches - -# 1. Build the development docker image (as per the documentation) -# 2. Launch the development server, and update the installation -# 3. Run unit tests within the docker context - -name: Docker Test - -on: - push: - branches: - - 'master' - - 'stable' - -jobs: - - docker: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Build Docker Image - run: | - cd docker - docker-compose build - docker-compose run inventree-dev-server invoke update - docker-compose up -d - - name: Wait for Server - run: | - cd docker - docker-compose run inventree-dev-server invoke wait - - name: Run unit tests - run: | - cd docker - docker-compose run inventree-dev-server invoke test From 3e05c5fde1a2fe52e5aa82d64a7477c4d392d5f6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 15:12:19 +1000 Subject: [PATCH 35/72] Bring docker containers down --- .github/workflows/docker_latest.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker_latest.yaml b/.github/workflows/docker_latest.yaml index 9942407c07..9324c39b54 100644 --- a/.github/workflows/docker_latest.yaml +++ b/.github/workflows/docker_latest.yaml @@ -32,6 +32,10 @@ jobs: run: | cd docker docker-compose run inventree-dev-server invoke test + - name: Down again + run: | + cd docker + docker-compose down - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx From 2fde482eab5463803773625e1c06ed1034f6cc16 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 15:39:58 +1000 Subject: [PATCH 36/72] Simplify steps --- .github/workflows/docker_latest.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker_latest.yaml b/.github/workflows/docker_latest.yaml index 9324c39b54..74b5eb966c 100644 --- a/.github/workflows/docker_latest.yaml +++ b/.github/workflows/docker_latest.yaml @@ -23,18 +23,12 @@ jobs: cd docker docker-compose build docker-compose run inventree-dev-server invoke update - docker-compose up -d - - name: Wait for Server - run: | - cd docker - docker-compose run inventree-dev-server invoke wait - name: Run unit tests run: | cd docker + docker-compose up -d + docker-compose run inventree-dev-server invoke wait docker-compose run inventree-dev-server invoke test - - name: Down again - run: | - cd docker docker-compose down - name: Set up QEMU uses: docker/setup-qemu-action@v1 From 6147afe35ff6c700e787e1032114db819cc2fb11 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 16:54:57 +1000 Subject: [PATCH 37/72] Catch errors when rendering custom plugin panels --- InvenTree/plugin/views.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/views.py b/InvenTree/plugin/views.py index ee855094cc..d03c28bf90 100644 --- a/InvenTree/plugin/views.py +++ b/InvenTree/plugin/views.py @@ -1,5 +1,10 @@ +import sys +import traceback from django.conf import settings +from django.views.debug import ExceptionReporter + +from error_report.models import Error from plugin.registry import registry @@ -21,7 +26,21 @@ class InvenTreePluginViewMixin: panels = [] for plug in registry.with_mixin('panel'): - panels += plug.render_panels(self, self.request, ctx) + + try: + panels += plug.render_panels(self, self.request, ctx) + except Exception as exc: + # Prevent any plugin error from crashing the page render + kind, info, data = sys.exc_info() + + # Log the error to the database + Error.objects.create( + kind=kind.__name__, + info=info, + data='\n'.join(traceback.format_exception(kind, info, data)), + path=self.request.path, + html=ExceptionReporter(self.request, kind, info, data).get_traceback_html(), + ) return panels From 4ceb35a43f063ccccfe30ef4b1a934a32f7e9c49 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 May 2022 17:00:20 +1000 Subject: [PATCH 38/72] Fix PEP issue --- InvenTree/plugin/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/views.py b/InvenTree/plugin/views.py index d03c28bf90..8d28872695 100644 --- a/InvenTree/plugin/views.py +++ b/InvenTree/plugin/views.py @@ -29,7 +29,7 @@ class InvenTreePluginViewMixin: try: panels += plug.render_panels(self, self.request, ctx) - except Exception as exc: + except Exception: # Prevent any plugin error from crashing the page render kind, info, data = sys.exc_info() From 67c675d1a6d66f6f908d3b19cd26f6ef10d38f7e Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Wed, 18 May 2022 13:24:50 +0200 Subject: [PATCH 39/72] Add ManufacturerPartAttachment class --- InvenTree/company/models.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 40afa6db6d..e33580abff 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -22,6 +22,7 @@ from stdimage.models import StdImageField from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail from InvenTree.fields import InvenTreeURLField +from InvenTree.models import InvenTreeAttachment from InvenTree.status_codes import PurchaseOrderStatus import InvenTree.validators @@ -380,6 +381,22 @@ class ManufacturerPart(models.Model): return s +class ManufacturerPartAttachment(InvenTreeAttachment): + """ + Model for storing file attachments against a ManufacturerPart object + """ + + @staticmethod + def get_api_url(): + return reverse('api-manufacturer-part-attachment-list') + + def getSubdir(self): + return os.path.join("manufacturer_part_files", str(self.manufacturer_part.id)) + + manufacturer_part = models.ForeignKey(ManufacturerPart, on_delete=models.CASCADE, + verbose_name=_('Manufacturer Part'), related_name='attachments') + + class ManufacturerPartParameter(models.Model): """ A ManufacturerPartParameter represents a key:value parameter for a MnaufacturerPart. From c608778a1b2469f15a9d5183e73fdc549435e66b Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 13:01:45 +0000 Subject: [PATCH 40/72] Add migration --- .../0043_manufacturerpartattachment.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 InvenTree/company/migrations/0043_manufacturerpartattachment.py diff --git a/InvenTree/company/migrations/0043_manufacturerpartattachment.py b/InvenTree/company/migrations/0043_manufacturerpartattachment.py new file mode 100644 index 0000000000..fe526992b0 --- /dev/null +++ b/InvenTree/company/migrations/0043_manufacturerpartattachment.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.13 on 2022-05-01 12:57 + +import InvenTree.fields +import InvenTree.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('company', '0042_supplierpricebreak_updated'), + ] + + operations = [ + migrations.CreateModel( + name='ManufacturerPartAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment')), + ('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link')), + ('comment', models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment')), + ('upload_date', models.DateField(auto_now_add=True, null=True, verbose_name='upload date')), + ('manufacturer_part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='company.manufacturerpart', verbose_name='Manufacturer Part')), + ('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'abstract': False, + }, + ), + ] From a373e669cd417c4c2e17746f1fec9597402c2c52 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 13:14:50 +0000 Subject: [PATCH 41/72] Add permission --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 7ed689f4a9..3c33614b97 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -101,6 +101,7 @@ class RuleSet(models.Model): 'company_supplierpart', 'company_manufacturerpart', 'company_manufacturerpartparameter', + 'company_manufacturerpartattachment', 'label_partlabel', ], 'stock_location': [ From 3ee32374b48b1ebef25f185d74d557a4239efe5a Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 13:15:37 +0000 Subject: [PATCH 42/72] Add serializer --- InvenTree/company/serializers.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 236dcc15db..54eeb2c191 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -8,6 +8,7 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount +from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeImageSerializerField from InvenTree.serializers import InvenTreeModelSerializer @@ -16,7 +17,7 @@ from InvenTree.serializers import InvenTreeMoneySerializer from part.serializers import PartBriefSerializer from .models import Company -from .models import ManufacturerPart, ManufacturerPartParameter +from .models import ManufacturerPart, ManufacturerPartAttachment, ManufacturerPartParameter from .models import SupplierPart, SupplierPriceBreak from common.settings import currency_code_default, currency_code_mappings @@ -142,6 +143,29 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer): ] +class ManufacturerPartAttachmentSerializer(InvenTreeAttachmentSerializer): + """ + Serializer for the ManufacturerPartAttachment class + """ + + class Meta: + model = ManufacturerPartAttachment + + fields = [ + 'pk', + 'manufacturer_part', + 'attachment', + 'filename', + 'link', + 'comment', + 'upload_date', + ] + + read_only_fields = [ + 'upload_date', + ] + + class ManufacturerPartParameterSerializer(InvenTreeModelSerializer): """ Serializer for the ManufacturerPartParameter model From 69ba271bf7ae4b78d01e663adaf495808cc14174 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 13:51:09 +0000 Subject: [PATCH 43/72] Add API endpoints --- InvenTree/company/api.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 146a45f648..c171bf7448 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -12,13 +12,14 @@ from django.urls import include, re_path from django.db.models import Q from InvenTree.helpers import str2bool +from InvenTree.api import AttachmentMixin from .models import Company -from .models import ManufacturerPart, ManufacturerPartParameter +from .models import ManufacturerPart, ManufacturerPartAttachment, ManufacturerPartParameter from .models import SupplierPart, SupplierPriceBreak from .serializers import CompanySerializer -from .serializers import ManufacturerPartSerializer, ManufacturerPartParameterSerializer +from .serializers import ManufacturerPartSerializer, ManufacturerPartAttachmentSerializer, ManufacturerPartParameterSerializer from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer @@ -160,6 +161,32 @@ class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = ManufacturerPartSerializer +class ManufacturerPartAttachmentList(AttachmentMixin, generics.ListCreateAPIView): + """ + API endpoint for listing (and creating) a ManufacturerPartAttachment (file upload). + """ + + queryset = ManufacturerPartAttachment.objects.all() + serializer_class = ManufacturerPartAttachmentSerializer + + filter_backends = [ + DjangoFilterBackend, + ] + + filter_fields = [ + 'manufacturer_part', + ] + + +class ManufacturerPartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): + """ + Detail endpooint for ManufacturerPartAttachment model + """ + + queryset = ManufacturerPartAttachment.objects.all() + serializer_class = ManufacturerPartAttachmentSerializer + + class ManufacturerPartParameterList(generics.ListCreateAPIView): """ API endpoint for list view of ManufacturerPartParamater model. From 09a76277888aef563b52a2d26a6c25617515f527 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 13:57:02 +0000 Subject: [PATCH 44/72] Add API URLs --- InvenTree/company/api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index c171bf7448..22c5c4b207 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -414,6 +414,12 @@ class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView): manufacturer_part_api_urls = [ + # Base URL for ManufacturerPartAttachment API endpoints + re_path(r'^attachment/', include([ + re_path(r'^(?P\d+)/', ManufacturerPartAttachmentDetail.as_view(), name='api-manufacturer-part-attachment-detail'), + re_path(r'^$', ManufacturerPartAttachmentList.as_view(), name='api-manufacturer-part-attachment-list'), + ])), + re_path(r'^parameter/', include([ re_path(r'^(?P\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'), From fc3e61df24290530b6a1cee67a1148d3573c883d Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 14:03:19 +0000 Subject: [PATCH 45/72] Add sidebar item --- .../company/templates/company/manufacturer_part_sidebar.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/company/templates/company/manufacturer_part_sidebar.html b/InvenTree/company/templates/company/manufacturer_part_sidebar.html index bd613f76aa..04f3a39a5b 100644 --- a/InvenTree/company/templates/company/manufacturer_part_sidebar.html +++ b/InvenTree/company/templates/company/manufacturer_part_sidebar.html @@ -4,5 +4,7 @@ {% trans "Parameters" as text %} {% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %} +{% trans "Attachments" as text %} +{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %} {% trans "Supplier Parts" as text %} {% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %} \ No newline at end of file From c6d3cd9bae5070f289eff971376094b89fb0cde4 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 14:12:00 +0000 Subject: [PATCH 46/72] Add content panel --- .../templates/company/manufacturer_part.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html index 5a0e741c1a..ae4690f1c6 100644 --- a/InvenTree/company/templates/company/manufacturer_part.html +++ b/InvenTree/company/templates/company/manufacturer_part.html @@ -144,6 +144,21 @@ src="{% static 'img/blank_image.png' %}" +
+
+
+

{% trans "Attachments" %}

+ {% include "spacer.html" %} +
+ {% include "attachment_button.html" %} +
+
+
+
+ {% include "attachment_table.html" %} +
+
+
From 72f330ab7553ea43769312ad7bf7a52e95a34b13 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 14:33:44 +0000 Subject: [PATCH 47/72] Add JS --- .../templates/company/manufacturer_part.html | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html index ae4690f1c6..a51ea45099 100644 --- a/InvenTree/company/templates/company/manufacturer_part.html +++ b/InvenTree/company/templates/company/manufacturer_part.html @@ -193,6 +193,34 @@ src="{% static 'img/blank_image.png' %}" {% block js_ready %} {{ block.super }} +onPanelLoad("attachments", function() { + loadAttachmentTable('{% url "api-manufacturer-part-attachment-list" %}', { + filters: { + manufacturer_part: {{ part.pk }}, + }, + fields: { + manufacturer_part: { + value: {{ part.pk }}, + hidden: true + } + } + }); + + enableDragAndDrop( + '#attachment-dropzone', + '{% url "api-manufacturer-part-attachment-list" %}', + { + data: { + manufacturer_part: {{ part.id }}, + }, + label: 'attachment', + success: function(data, status, xhr) { + reloadAttachmentTable(); + } + } + ); +}); + function reloadParameters() { $("#parameter-table").bootstrapTable("refresh"); } From ed1cc1209e8c47f3dadb31d5381babf6e3117eab Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 1 May 2022 17:48:55 +0000 Subject: [PATCH 48/72] Add admin class --- InvenTree/company/admin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index cc672f9ee5..ce5f5945b6 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -8,7 +8,7 @@ import import_export.widgets as widgets from .models import Company from .models import SupplierPart from .models import SupplierPriceBreak -from .models import ManufacturerPart, ManufacturerPartParameter +from .models import ManufacturerPart, ManufacturerPartAttachment, ManufacturerPartParameter from part.models import Part @@ -109,6 +109,16 @@ class ManufacturerPartAdmin(ImportExportModelAdmin): autocomplete_fields = ('part', 'manufacturer',) +class ManufacturerPartAttachmentAdmin(ImportExportModelAdmin): + """ + Admin class for ManufacturerPartAttachment model + """ + + list_display = ('manufacturer_part', 'attachment', 'comment') + + autocomplete_fields = ('manufacturer_part',) + + class ManufacturerPartParameterResource(ModelResource): """ Class for managing ManufacturerPartParameter data import/export @@ -175,4 +185,5 @@ admin.site.register(SupplierPart, SupplierPartAdmin) admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin) admin.site.register(ManufacturerPart, ManufacturerPartAdmin) +admin.site.register(ManufacturerPartAttachment, ManufacturerPartAttachmentAdmin) admin.site.register(ManufacturerPartParameter, ManufacturerPartParameterAdmin) From 3f67682d53e4f0235acc1054aa67c1b9ca78dd56 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Wed, 18 May 2022 13:22:57 +0200 Subject: [PATCH 49/72] Increment API version --- InvenTree/InvenTree/api_version.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index d2eab15468..e44aedf10b 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -4,11 +4,14 @@ InvenTree API version information # InvenTree API version -INVENTREE_API_VERSION = 49 +INVENTREE_API_VERSION = 50 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v50 -> 2022-05-18 : https://github.com/inventree/InvenTree/pull/2912 + - Implement Attachments for manufacturer parts + v49 -> 2022-05-09 : https://github.com/inventree/InvenTree/pull/2957 - Allows filtering of plugin list by 'active' status - Allows filtering of plugin list by 'mixin' support From aa3a86f3728d69aa3132be14513756d3b729fa3b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 21:33:40 +1000 Subject: [PATCH 50/72] 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 0e0ba66b9a2c87a85c889872c1ddda860840fbdd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 21:40:53 +1000 Subject: [PATCH 51/72] Fix broken calls to offload_task --- InvenTree/plugin/base/integration/mixins.py | 3 ++- InvenTree/plugin/base/locate/api.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 86e3092e4f..64de5df22b 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -13,6 +13,7 @@ import InvenTree.helpers from plugin.helpers import MixinImplementationError, MixinNotImplementedError, render_template from plugin.models import PluginConfig, PluginSetting +from plugin.registry import registry from plugin.urls import PLUGIN_BASE @@ -204,7 +205,7 @@ class ScheduleMixin: Schedule.objects.create( name=task_name, - func='plugin.registry.call_function', + func=registry.call_plugin_function, args=f"'{slug}', '{func_name}'", schedule_type=task['schedule'], minutes=task.get('minutes', None), diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index a6776f2d40..f617ba3577 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -7,7 +7,7 @@ from rest_framework.views import APIView from InvenTree.tasks import offload_task -from plugin import registry +from plugin.registry import registry from stock.models import StockItem, StockLocation @@ -53,7 +53,7 @@ class LocatePluginView(APIView): try: StockItem.objects.get(pk=item_pk) - offload_task(registry.call_function, plugin, 'locate_stock_item', item_pk) + offload_task(registry.call_plugin_function, plugin, 'locate_stock_item', item_pk) data['item'] = item_pk @@ -66,7 +66,7 @@ class LocatePluginView(APIView): try: StockLocation.objects.get(pk=location_pk) - offload_task(registry.call_function, plugin, 'locate_stock_location', location_pk) + offload_task(registry.call_plugin_function, plugin, 'locate_stock_location', location_pk) data['location'] = location_pk From c7003fbed85c864177cd3472ac6b7e14e214b433 Mon Sep 17 00:00:00 2001 From: Maksim Stojkovic <18454392+maksimstojkovic@users.noreply.github.com> Date: Wed, 18 May 2022 22:01:47 +1000 Subject: [PATCH 52/72] Create a default shipment when creating SO --- InvenTree/templates/js/translated/order.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index e2bee865fd..35bd791382 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -362,6 +362,17 @@ function createSalesOrder(options={}) { } }, onSuccess: function(data) { + inventreePut( + '{% url "api-so-shipment-list" %}', + { + order: data.pk, + reference: 1 + }, + { + method: 'POST' + } + ); + location.href = `/order/sales-order/${data.pk}/`; }, title: '{% trans "Create Sales Order" %}', From dd476ce796103f2a8fe84a0be240604249a060a8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 22:20:29 +1000 Subject: [PATCH 53/72] Add unit tests for the 'locate' plugin - Test various failure modes - Some of the failure modes didn't fail - this is also a failure - Fixing API code accordingly --- InvenTree/plugin/base/locate/api.py | 14 ++-- InvenTree/plugin/base/locate/test_locate.py | 89 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 InvenTree/plugin/base/locate/test_locate.py diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index f617ba3577..a7effe91b3 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -40,9 +40,6 @@ class LocatePluginView(APIView): # StockLocation to identify location_pk = request.data.get('location', None) - if not item_pk and not location_pk: - raise ParseError("Must supply either 'item' or 'location' parameter") - data = { "success": "Identification plugin activated", "plugin": plugin, @@ -59,8 +56,8 @@ class LocatePluginView(APIView): return Response(data) - except StockItem.DoesNotExist: - raise NotFound("StockItem matching PK '{item}' not found") + except (ValueError, StockItem.DoesNotExist): + raise NotFound(f"StockItem matching PK '{item_pk}' not found") elif location_pk: try: @@ -72,8 +69,9 @@ class LocatePluginView(APIView): return Response(data) - except StockLocation.DoesNotExist: - raise NotFound("StockLocation matching PK {'location'} not found") + except (ValueError, StockLocation.DoesNotExist): + raise NotFound(f"StockLocation matching PK '{location_pk}' not found") else: - raise NotFound() + raise ParseError("Must supply either 'item' or 'location' parameter") + diff --git a/InvenTree/plugin/base/locate/test_locate.py b/InvenTree/plugin/base/locate/test_locate.py new file mode 100644 index 0000000000..26fafcca49 --- /dev/null +++ b/InvenTree/plugin/base/locate/test_locate.py @@ -0,0 +1,89 @@ +""" +Unit tests for the 'locate' plugin mixin class +""" + +from django.urls import reverse + +from InvenTree.api_tester import InvenTreeAPITestCase + +from plugin.registry import registry + + +class LocatePluginTests(InvenTreeAPITestCase): + + fixtures = [ + 'category', + 'part', + 'location', + 'stock', + ] + + def test_installed(self): + """Test that a locate plugin is actually installed""" + + plugins = registry.with_mixin('locate') + + self.assertTrue(len(plugins) > 0) + + self.assertTrue('samplelocate' in [p.slug for p in plugins]) + + def test_locate_fail(self): + """Test various API failure modes""" + + url = reverse('api-locate-plugin') + + # Post without a plugin + response = self.post( + url, + {}, + expected_code=400 + ) + + self.assertIn("'plugin' field must be supplied", str(response.data)) + + # Post with a plugin that does not exist, or is invalid + for slug in ['xyz', 'event', 'plugin']: + response = self.post( + url, + { + 'plugin': slug, + }, + expected_code=400, + ) + + self.assertIn(f"Plugin '{slug}' is not installed, or does not support the location mixin", str(response.data)) + + # Post with a valid plugin, but no other data + response = self.post( + url, + { + 'plugin': 'samplelocate', + }, + expected_code=400 + ) + + self.assertIn("Must supply either 'item' or 'location' parameter", str(response.data)) + + # Post with valid plugin, invalid item or location + for pk in ['qq', 99999, -42]: + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'item': pk, + }, + expected_code=404 + ) + + self.assertIn(f"StockItem matching PK '{pk}' not found", str(response.data)) + + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'location': pk, + }, + expected_code=404, + ) + + self.assertIn(f"StockLocation matching PK '{pk}' not found", str(response.data)) \ No newline at end of file From c6590066b865416e5761718e2a61dca06ad44e81 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 22:46:15 +1000 Subject: [PATCH 54/72] Add tests for successful location - Sample plugin now updates metadata tag --- InvenTree/plugin/base/locate/api.py | 1 - InvenTree/plugin/base/locate/test_locate.py | 63 ++++++++++++++++++- .../plugin/samples/locate/locate_sample.py | 24 ++++++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index a7effe91b3..3004abb262 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -74,4 +74,3 @@ class LocatePluginView(APIView): else: raise ParseError("Must supply either 'item' or 'location' parameter") - diff --git a/InvenTree/plugin/base/locate/test_locate.py b/InvenTree/plugin/base/locate/test_locate.py index 26fafcca49..e145c2360b 100644 --- a/InvenTree/plugin/base/locate/test_locate.py +++ b/InvenTree/plugin/base/locate/test_locate.py @@ -7,6 +7,7 @@ from django.urls import reverse from InvenTree.api_tester import InvenTreeAPITestCase from plugin.registry import registry +from stock.models import StockItem, StockLocation class LocatePluginTests(InvenTreeAPITestCase): @@ -29,7 +30,7 @@ class LocatePluginTests(InvenTreeAPITestCase): def test_locate_fail(self): """Test various API failure modes""" - + url = reverse('api-locate-plugin') # Post without a plugin @@ -86,4 +87,62 @@ class LocatePluginTests(InvenTreeAPITestCase): expected_code=404, ) - self.assertIn(f"StockLocation matching PK '{pk}' not found", str(response.data)) \ No newline at end of file + self.assertIn(f"StockLocation matching PK '{pk}' not found", str(response.data)) + + def test_locate_item(self): + """ + Test that the plugin correctly 'locates' a StockItem + + As the background worker is not running during unit testing, + the sample 'locate' function will be called 'inline' + """ + + url = reverse('api-locate-plugin') + + item = StockItem.objects.get(pk=1) + + # The sample plugin will set the 'located' metadata tag + item.set_metadata('located', False) + + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'item': 1, + }, + expected_code=200 + ) + + self.assertEqual(response.data['item'], 1) + + item.refresh_from_db() + + # Item metadata should have been altered! + self.assertTrue(item.metadata['located']) + + def test_locate_location(self): + """ + Test that the plugin correctly 'locates' a StockLocation + """ + + url = reverse('api-locate-plugin') + + for location in StockLocation.objects.all(): + + location.set_metadata('located', False) + + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'location': location.pk, + }, + expected_code=200 + ) + + self.assertEqual(response.data['location'], location.pk) + + location.refresh_from_db() + + # Item metadata should have been altered! + self.assertTrue(location.metadata['located']) diff --git a/InvenTree/plugin/samples/locate/locate_sample.py b/InvenTree/plugin/samples/locate/locate_sample.py index 458b84cfa5..32a2dd713c 100644 --- a/InvenTree/plugin/samples/locate/locate_sample.py +++ b/InvenTree/plugin/samples/locate/locate_sample.py @@ -23,7 +23,23 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): SLUG = "samplelocate" TITLE = "Sample plugin for locating items" - VERSION = "0.1" + VERSION = "0.2" + + def locate_stock_item(self, item_pk): + + from stock.models import StockItem + + logger.info(f"SampleLocatePlugin attempting to locate item ID {item_pk}") + + try: + item = StockItem.objects.get(pk=item_pk) + logger.info(f"StockItem {item_pk} located!") + + # Tag metadata + item.set_metadata('located', True) + + except (ValueError, StockItem.DoesNotExist): + logger.error(f"StockItem ID {item_pk} does not exist!") def locate_stock_location(self, location_pk): @@ -34,5 +50,9 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): try: location = StockLocation.objects.get(pk=location_pk) logger.info(f"Location exists at '{location.pathstring}'") - except StockLocation.DoesNotExist: + + # Tag metadata + location.set_metadata('located', True) + + except (ValueError, StockLocation.DoesNotExist): logger.error(f"Location ID {location_pk} does not exist!") From 9ae8a6b21925318fc525b0674b208e8afcd31a1a Mon Sep 17 00:00:00 2001 From: Maksim Stojkovic <18454392+maksimstojkovic@users.noreply.github.com> Date: Wed, 18 May 2022 23:43:06 +1000 Subject: [PATCH 55/72] Revert "Create a default shipment when creating SO" This reverts commit c7003fbed85c864177cd3472ac6b7e14e214b433. --- InvenTree/templates/js/translated/order.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 35bd791382..e2bee865fd 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -362,17 +362,6 @@ function createSalesOrder(options={}) { } }, onSuccess: function(data) { - inventreePut( - '{% url "api-so-shipment-list" %}', - { - order: data.pk, - reference: 1 - }, - { - method: 'POST' - } - ); - location.href = `/order/sales-order/${data.pk}/`; }, title: '{% trans "Create Sales Order" %}', From 2cf67ea0c977fdaae0b2cbd0eaec076e7585455f Mon Sep 17 00:00:00 2001 From: Maksim Stojkovic <18454392+maksimstojkovic@users.noreply.github.com> Date: Thu, 19 May 2022 00:10:55 +1000 Subject: [PATCH 56/72] Create default shipment in backend --- InvenTree/common/models.py | 7 +++++++ InvenTree/order/models.py | 16 ++++++++++++++++ InvenTree/templates/InvenTree/settings/so.html | 1 + 3 files changed, 24 insertions(+) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 83773fe48a..0ef766d19d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1111,6 +1111,13 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'default': 'SO', }, + 'SALESORDER_DEFAULT_SHIPMENT': { + 'name': _('Sales Order Default Shipment'), + 'description': _('Enable creation of default shipment with sales orders'), + 'default': False, + 'validator': bool, + }, + 'PURCHASEORDER_REFERENCE_PREFIX': { 'name': _('Purchase Order Reference Prefix'), 'description': _('Prefix value for purchase order reference'), diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7460e81e56..58f4fd5c2f 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -12,6 +12,8 @@ from decimal import Decimal from django.db import models, transaction from django.db.models import Q, F, Sum from django.db.models.functions import Coalesce +from django.db.models.signals import post_save +from django.dispatch.dispatcher import receiver from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError @@ -808,6 +810,20 @@ class SalesOrder(Order): def pending_shipment_count(self): return self.pending_shipments().count() +@receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log') +def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs): + """ + Callback function to be executed after a SalesOrder instance is saved + """ + if created and getSetting('SALESORDER_DEFAULT_SHIPMENT'): + # A new SalesOrder has just been created + + # Create default shipment + SalesOrderShipment.objects.create( + order=instance, + reference='1', + ) + class PurchaseOrderAttachment(InvenTreeAttachment): """ diff --git a/InvenTree/templates/InvenTree/settings/so.html b/InvenTree/templates/InvenTree/settings/so.html index e6fde3a093..ac84f5fa86 100644 --- a/InvenTree/templates/InvenTree/settings/so.html +++ b/InvenTree/templates/InvenTree/settings/so.html @@ -12,6 +12,7 @@ {% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PREFIX" %} + {% include "InvenTree/settings/setting.html" with key="SALESORDER_DEFAULT_SHIPMENT" icon="fa-truck-loading" %}
From 276075ce0508b3f6dfb35a9df41f3094966fb8aa Mon Sep 17 00:00:00 2001 From: Maksim Stojkovic <18454392+maksimstojkovic@users.noreply.github.com> Date: Thu, 19 May 2022 00:33:03 +1000 Subject: [PATCH 57/72] PEP styling --- InvenTree/common/models.py | 2 +- InvenTree/order/models.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 0ef766d19d..92e5c1522c 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1117,7 +1117,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'default': False, 'validator': bool, }, - + 'PURCHASEORDER_REFERENCE_PREFIX': { 'name': _('Purchase Order Reference Prefix'), 'description': _('Prefix value for purchase order reference'), diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 58f4fd5c2f..f4688f3736 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -810,6 +810,7 @@ class SalesOrder(Order): def pending_shipment_count(self): return self.pending_shipments().count() + @receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log') def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs): """ From 0831b85e29cb94f892f2420cec7b7da20282e86e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 19 May 2022 01:39:16 +1000 Subject: [PATCH 58/72] 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""" From 4dc997c4aecbfd6fd7ab5db8944bbbb50c30c2f7 Mon Sep 17 00:00:00 2001 From: Maksim Stojkovic <18454392+maksimstojkovic@users.noreply.github.com> Date: Thu, 19 May 2022 02:06:43 +1000 Subject: [PATCH 59/72] Added unit test --- InvenTree/order/test_sales_order.py | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index d43c94996c..954fecf2fe 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -10,6 +10,8 @@ from company.models import Company from InvenTree import status_codes as status +from common.models import InvenTreeSetting + from order.models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation, SalesOrderShipment from part.models import Part @@ -200,3 +202,38 @@ class SalesOrderTest(TestCase): self.assertTrue(self.line.is_fully_allocated()) self.assertEqual(self.line.fulfilled_quantity(), 50) self.assertEqual(self.line.allocated_quantity(), 50) + + def test_default_shipment(self): + # Test sales order default shipment creation + + # Default setting value should be False + self.assertEqual(False, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT')) + + # Create an order + order_1 = SalesOrder.objects.create( + customer=self.customer, + reference='1235', + customer_reference='ABC 55556' + ) + + # Order should have no shipments when setting is False + self.assertEqual(0, order_1.shipment_count) + + # Update setting to True + InvenTreeSetting.set_setting('SALESORDER_DEFAULT_SHIPMENT', True, None) + self.assertEqual(True, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT')) + + # Create a second order + order_2 = SalesOrder.objects.create( + customer=self.customer, + reference='1236', + customer_reference='ABC 55557' + ) + + # Order should have one shipment + self.assertEqual(1, order_2.shipment_count) + self.assertEqual(1, order_2.pending_shipments().count()) + + # Shipment should have default reference of '1' + self.assertEqual('1', order_2.pending_shipments()[0].reference) + \ No newline at end of file From 9e8da0e8e06e1cd359340b8f0475a058ff8fad62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A1lm=C3=A1n=20R=C3=B3zsahegyi?= Date: Wed, 18 May 2022 22:20:42 +0200 Subject: [PATCH 60/72] Fix manufacturer part detail template with missing company view tests --- .../templates/company/manufacturer_part.html | 1 - InvenTree/company/test_views.py | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html index a51ea45099..aa087fe207 100644 --- a/InvenTree/company/templates/company/manufacturer_part.html +++ b/InvenTree/company/templates/company/manufacturer_part.html @@ -94,7 +94,6 @@ src="{% static 'img/blank_image.png' %}" {% else %} {% trans "No manufacturer information available" %} {% endif %} - {% endif %} diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py index 6d28e85d23..29900236bf 100644 --- a/InvenTree/company/test_views.py +++ b/InvenTree/company/test_views.py @@ -55,3 +55,29 @@ class CompanyViewTest(CompanyViewTestBase): response = self.client.get(reverse('company-index')) self.assertEqual(response.status_code, 200) + + def test_manufacturer_index(self): + """ Test the manufacturer index """ + + response = self.client.get(reverse('manufacturer-index')) + self.assertEqual(response.status_code, 200) + + def test_customer_index(self): + """ Test the customer index """ + + response = self.client.get(reverse('customer-index')) + self.assertEqual(response.status_code, 200) + + def test_manufacturer_part_detail_view(self): + """ Test the manufacturer part detail view """ + + response = self.client.get(reverse('manufacturer-part-detail', kwargs={'pk': 1})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'MPN123') + + def test_supplier_part_detail_view(self): + """ Test the supplier part detail view """ + + response = self.client.get(reverse('supplier-part-detail', kwargs={'pk': 10})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'MPN456-APPEL') From 9446702d78bc34900ebb338f34f79c032467463a Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 09:36:14 +1000 Subject: [PATCH 61/72] Skip plugin loading for various database admin functions --- InvenTree/InvenTree/ready.py | 3 ++- InvenTree/plugin/apps.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/ready.py b/InvenTree/InvenTree/ready.py index 9f5ad0ea49..e93972cf2e 100644 --- a/InvenTree/InvenTree/ready.py +++ b/InvenTree/InvenTree/ready.py @@ -39,7 +39,8 @@ def canAppAccessDatabase(allow_test=False): 'createsuperuser', 'wait_for_db', 'prerender', - 'rebuild', + 'rebuild_models', + 'rebuild_thumbnails', 'collectstatic', 'makemessages', 'compilemessages', diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 233f037ec7..a176612fb6 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from maintenance_mode.core import set_maintenance_mode -from InvenTree.ready import isImportingData +from InvenTree.ready import canAppAccessDatabase from plugin import registry from plugin.helpers import check_git_version, log_error @@ -20,9 +20,8 @@ class PluginAppConfig(AppConfig): def ready(self): if settings.PLUGINS_ENABLED: - - if isImportingData(): # pragma: no cover - logger.info('Skipping plugin loading for data import') + if not canAppAccessDatabase(allow_test=True): + logger.info("Skipping plugin loading sequence") else: logger.info('Loading InvenTree plugins') @@ -48,3 +47,6 @@ class PluginAppConfig(AppConfig): registry.git_is_modern = check_git_version() if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load') + + else: + logger.info("Plugins not enabled - skipping loading sequence") \ No newline at end of file From 7d9690b974263ba499d026eabee504b5bd6cb8ac Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 09:53:12 +1000 Subject: [PATCH 62/72] Add logging message when plugin fails to render custom panels --- InvenTree/plugin/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/plugin/views.py b/InvenTree/plugin/views.py index 8d28872695..9b12ef12fe 100644 --- a/InvenTree/plugin/views.py +++ b/InvenTree/plugin/views.py @@ -1,3 +1,4 @@ +import logging import sys import traceback @@ -9,6 +10,9 @@ from error_report.models import Error from plugin.registry import registry +logger = logging.getLogger('inventree') + + class InvenTreePluginViewMixin: """ Custom view mixin which adds context data to the view, @@ -42,6 +46,8 @@ class InvenTreePluginViewMixin: html=ExceptionReporter(self.request, kind, info, data).get_traceback_html(), ) + logger.error(f"Plugin '{plug.slug}' could not render custom panels at '{self.request.path}'") + return panels def get_context_data(self, **kwargs): From 14b60cdedcde7623fd15e4471ead66a5febe9386 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 10:03:44 +1000 Subject: [PATCH 63/72] Custom panel content gets passed through the templating engine --- InvenTree/plugin/base/integration/mixins.py | 9 ++++++++- InvenTree/plugin/helpers.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 64de5df22b..c347d6c406 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -11,7 +11,8 @@ from django.db.utils import OperationalError, ProgrammingError import InvenTree.helpers -from plugin.helpers import MixinImplementationError, MixinNotImplementedError, render_template +from plugin.helpers import MixinImplementationError, MixinNotImplementedError +from plugin.helpers import render_template, render_text from plugin.models import PluginConfig, PluginSetting from plugin.registry import registry from plugin.urls import PLUGIN_BASE @@ -578,10 +579,16 @@ class PanelMixin: if content_template: # Render content template to HTML panel['content'] = render_template(self, content_template, ctx) + else: + # Render content string to HTML + panel['content'] = render_text(panel.get('content', ''), ctx) if javascript_template: # Render javascript template to HTML panel['javascript'] = render_template(self, javascript_template, ctx) + else: + # Render javascript string to HTML + panel['javascript'] = render_text(panel.get('javascript', ''), ctx) # Check for required keys required_keys = ['title', 'content'] diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 1217fa4d47..90ffe61478 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -245,4 +245,15 @@ def render_template(plugin, template_file, context=None): html = tmp.render(context) return html + + +def render_text(text, context=None): + """ + Locate a raw string with provided context + """ + + ctx = template.Context(context) + + return template.Template(text).render(ctx) + # endregion From ebcb9685b56dc293bfca2a07bb8c139e75f39abd Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 10:04:20 +1000 Subject: [PATCH 64/72] Updates to samplepanel plugin - Enhanced content for "hello world" panel - Add an optional panel which breaks rendering --- .../plugin/samples/event/event_sample.py | 2 +- .../integration/custom_panel_sample.py | 39 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/samples/event/event_sample.py b/InvenTree/plugin/samples/event/event_sample.py index 5411781e05..bea21c3ea0 100644 --- a/InvenTree/plugin/samples/event/event_sample.py +++ b/InvenTree/plugin/samples/event/event_sample.py @@ -12,7 +12,7 @@ class EventPluginSample(EventMixin, InvenTreePlugin): """ NAME = "EventPlugin" - SLUG = "event" + SLUG = "sampleevent" TITLE = "Triggered Events" def process_event(self, event, *args, **kwargs): diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index 0203fc4e04..dd84a2a86f 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -15,17 +15,23 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin): """ NAME = "CustomPanelExample" - SLUG = "panel" + SLUG = "samplepanel" TITLE = "Custom Panel Example" DESCRIPTION = "An example plugin demonstrating how custom panels can be added to the user interface" VERSION = "0.1" SETTINGS = { 'ENABLE_HELLO_WORLD': { - 'name': 'Hello World', + 'name': 'Enable Hello World', 'description': 'Enable a custom hello world panel on every page', 'default': False, 'validator': bool, + }, + 'ENABLE_BROKEN_PANEL': { + 'name': 'Enable Broken Panel', + 'description': 'Enable a panel with rendering issues', + 'default': False, + 'validator': bool, } } @@ -58,15 +64,42 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin): ] if self.get_setting('ENABLE_HELLO_WORLD'): + + # We can use template rendering in the raw content + content = """ + Hello world! +
+
+ We can render custom content using the templating system! +
+
+ + + +
Path{{ request.path }}
User{{ user.username }}
+ """ + panels.append({ # This 'hello world' panel will be displayed on any view which implements custom panels 'title': 'Hello World', 'icon': 'fas fa-boxes', - 'content': 'Hello world!', + 'content': content, 'description': 'A simple panel which renders hello world', 'javascript': 'console.log("Hello world, from a custom panel!");', }) + if self.get_setting('ENABLE_BROKEN_PANEL'): + + # Enabling this panel will cause panel rendering to break, + # due to the invalid tags + panels.append({ + 'title': 'Broken Panel', + 'icon': 'fas fa-times-circle', + 'content': '{% tag_not_loaded %}', + 'description': 'This panel is broken', + 'javascript': '{% another_bad_tag %}', + }) + # This panel will *only* display on the PartDetail view if isinstance(view, PartDetail): panels.append({ From 11b21a9cca8a7a9e2f7493caeda3038264196036 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 11:00:31 +1000 Subject: [PATCH 65/72] Allow registry.with_mixin to filter by active status --- InvenTree/plugin/base/integration/mixins.py | 1 + InvenTree/plugin/registry.py | 10 +++++++++- .../plugin/samples/integration/custom_panel_sample.py | 2 +- InvenTree/plugin/views.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index c347d6c406..6977ef3dd9 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -60,6 +60,7 @@ class SettingsMixin: if not plugin: # Cannot find associated plugin model, return + logger.error(f"Plugin configuration not found for plugin '{self.slug}'") return # pragma: no cover PluginSetting.set_setting(key, value, user, plugin=plugin) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 3d58634340..d97fa73923 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -243,7 +243,7 @@ class PluginsRegistry: # endregion # region registry functions - def with_mixin(self, mixin: str): + def with_mixin(self, mixin: str, active=None): """ Returns reference to all plugins that have a specified mixin enabled """ @@ -251,6 +251,14 @@ class PluginsRegistry: for plugin in self.plugins.values(): if plugin.mixin_enabled(mixin): + + if active is not None: + # Filter by 'enabled' status + config = plugin.plugin_config() + + if config.active != active: + continue + result.append(plugin) return result diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index dd84a2a86f..3d44bc0c5b 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -58,7 +58,7 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin): panels = [ { - # This panel will not be displayed, as it is missing the 'content' key + # Simple panel without any actual content 'title': 'No Content', } ] diff --git a/InvenTree/plugin/views.py b/InvenTree/plugin/views.py index 9b12ef12fe..ad4d54daea 100644 --- a/InvenTree/plugin/views.py +++ b/InvenTree/plugin/views.py @@ -29,7 +29,7 @@ class InvenTreePluginViewMixin: panels = [] - for plug in registry.with_mixin('panel'): + for plug in registry.with_mixin('panel', active=True): try: panels += plug.render_panels(self, self.request, ctx) From 80e3d0970a5b938c0ff3f5459aa745a8bbe42aed Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 11:28:18 +1000 Subject: [PATCH 66/72] Adds unit tests for the samplepanel plugin --- InvenTree/plugin/apps.py | 2 +- .../plugin/base/integration/test_mixins.py | 158 +++++++++++++++++- InvenTree/plugin/registry.py | 2 +- setup.cfg | 2 +- 4 files changed, 160 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index a176612fb6..c0e894fef1 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -49,4 +49,4 @@ class PluginAppConfig(AppConfig): log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load') else: - logger.info("Plugins not enabled - skipping loading sequence") \ No newline at end of file + logger.info("Plugins not enabled - skipping loading sequence") diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index ef3f7062e3..4768020bf1 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -2,14 +2,17 @@ from django.test import TestCase from django.conf import settings -from django.urls import include, re_path +from django.urls import include, re_path, reverse from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from plugin import InvenTreePlugin from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin from plugin.urls import PLUGIN_BASE from plugin.helpers import MixinNotImplementedError +from plugin.registry import registry + class BaseMixinDefinition: def test_mixin_name(self): @@ -244,3 +247,156 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # cover wrong token setting with self.assertRaises(MixinNotImplementedError): self.mixin_wrong2.has_api_call() + + +class PanelMixinTests(TestCase): + """Test that the PanelMixin plugin operates correctly""" + + fixtures = [ + 'category', + 'part', + 'location', + 'stock', + ] + + def setUp(self): + super().setUp() + + # Create a user which has all the privelages + user = get_user_model() + + self.user = user.objects.create_user( + username='username', + email='user@email.com', + password='password' + ) + + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) + + # Give the group *all* the permissions! + for rule in group.rule_sets.all(): + rule.can_view = True + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + self.client.login(username='username', password='password') + + def test_installed(self): + """Test that the sample panel plugin is installed""" + + plugins = registry.with_mixin('panel') + + self.assertTrue(len(plugins) > 0) + + self.assertIn('samplepanel', [p.slug for p in plugins]) + + plugins = registry.with_mixin('panel', active=True) + + self.assertEqual(len(plugins), 0) + + def test_disabled(self): + """Test that the panels *do not load* if the plugin is not enabled""" + + plugin = registry.get_plugin('samplepanel') + + plugin.set_setting('ENABLE_HELLO_WORLD', True) + plugin.set_setting('ENABLE_BROKEN_PANEL', True) + + # Ensure that the plugin is *not* enabled + config = plugin.plugin_config() + + self.assertFalse(config.active) + + # Load some pages, ensure that the panel content is *not* loaded + for url in [ + reverse('part-detail', kwargs={'pk': 1}), + reverse('stock-item-detail', kwargs={'pk': 2}), + reverse('stock-location-detail', kwargs={'pk': 1}), + ]: + response = self.client.get( + url + ) + + self.assertEqual(response.status_code, 200) + + # Test that these panels have *not* been loaded + self.assertNotIn('No Content', str(response.content)) + self.assertNotIn('Hello world', str(response.content)) + self.assertNotIn('Custom Part Panel', str(response.content)) + + def test_enabled(self): + """ + Test that the panels *do* load if the plugin is enabled + """ + + plugin = registry.get_plugin('samplepanel') + + self.assertEqual(len(registry.with_mixin('panel', active=True)), 0) + + # Ensure that the plugin is enabled + config = plugin.plugin_config() + config.active = True + config.save() + + self.assertTrue(config.active) + self.assertEqual(len(registry.with_mixin('panel', active=True)), 1) + + # Load some pages, ensure that the panel content is *not* loaded + urls = [ + reverse('part-detail', kwargs={'pk': 1}), + reverse('stock-item-detail', kwargs={'pk': 2}), + reverse('stock-location-detail', kwargs={'pk': 1}), + ] + + plugin.set_setting('ENABLE_HELLO_WORLD', False) + plugin.set_setting('ENABLE_BROKEN_PANEL', False) + + for url in urls: + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + + self.assertIn('No Content', str(response.content)) + + # This panel is disabled by plugin setting + self.assertNotIn('Hello world!', str(response.content)) + + # This panel is only active for the "Part" view + if url == urls[0]: + self.assertIn('Custom Part Panel', str(response.content)) + else: + self.assertNotIn('Custom Part Panel', str(response.content)) + + # Enable the 'Hello World' panel + plugin.set_setting('ENABLE_HELLO_WORLD', True) + + for url in urls: + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + + self.assertIn('Hello world!', str(response.content)) + + # The 'Custom Part' panel should still be there, too + if url == urls[0]: + self.assertIn('Custom Part Panel', str(response.content)) + else: + self.assertNotIn('Custom Part Panel', str(response.content)) + + # Enable the 'broken panel' setting - this will cause all panels to not render + plugin.set_setting('ENABLE_BROKEN_PANEL', True) + + for url in urls: + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # No custom panels should have been loaded + self.assertNotIn('No Content', str(response.content)) + self.assertNotIn('Hello world!', str(response.content)) + self.assertNotIn('Broken Panel', str(response.content)) + self.assertNotIn('Custom Part Panel', str(response.content)) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index d97fa73923..1ec5adb161 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -255,7 +255,7 @@ class PluginsRegistry: if active is not None: # Filter by 'enabled' status config = plugin.plugin_config() - + if config.active != active: continue diff --git a/setup.cfg b/setup.cfg index a483481f5d..0aeaf4d01b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ ignore = N806, # - N812 - lowercase imported as non-lowercase N812, -exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/* +exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*,InvenTree/plugins/* max-complexity = 20 [coverage:run] From af88f6ec979fae4835ba06deaa8dbe29c3d656f7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 11:55:53 +1000 Subject: [PATCH 67/72] python CI: wait for server before continuing --- .github/workflows/qc_checks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index e73a1e8f98..93b208451b 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -153,6 +153,7 @@ jobs: invoke delete-data -f invoke import-fixtures invoke server -a 127.0.0.1:12345 & + invoke wait - name: Run Tests run: | cd ${{ env.wrapper_name }} From 140006a4cdefefdb37eb9a6f1b6293fb2c4e2a1b Mon Sep 17 00:00:00 2001 From: Maksim Stojkovic <18454392+maksimstojkovic@users.noreply.github.com> Date: Thu, 19 May 2022 12:19:55 +1000 Subject: [PATCH 68/72] Fix PEP styles --- InvenTree/order/test_sales_order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index 954fecf2fe..cbd572e24d 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -236,4 +236,3 @@ class SalesOrderTest(TestCase): # Shipment should have default reference of '1' self.assertEqual('1', order_2.pending_shipments()[0].reference) - \ No newline at end of file From adaec90909d2a081d75f756df0be11898167507b Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 12:54:07 +1000 Subject: [PATCH 69/72] CI: Allow exchange rate test a few goes --- InvenTree/InvenTree/tests.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 501eed0834..26b50a0eca 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -1,5 +1,6 @@ import json import os +import time from unittest import mock @@ -406,11 +407,23 @@ class CurrencyTests(TestCase): with self.assertRaises(MissingRate): convert_money(Money(100, 'AUD'), 'USD') - InvenTree.tasks.update_exchange_rates() + update_successful = False - rates = Rate.objects.all() + # Note: the update sometimes fails in CI, let's give it a few chances + for idx in range(10): + InvenTree.tasks.update_exchange_rates() - self.assertEqual(rates.count(), len(currency_codes())) + rates = Rate.objects.all() + + if rates.count() == len(currency_codes()): + update_successful = True + break + + else: + print("Exchange rate update failed - retrying") + time.sleep(1) + + self.assertTrue(update_successful) # Now that we have some exchange rate information, we can perform conversions From 07319731d20e7dbffc106bda2a86517040be0734 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 13:20:42 +1000 Subject: [PATCH 70/72] Validate that errors get logged --- InvenTree/plugin/base/integration/test_mixins.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 4768020bf1..c1afa39fc2 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -6,6 +6,8 @@ from django.urls import include, re_path, reverse from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from error_report.models import Error + from plugin import InvenTreePlugin from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin from plugin.urls import PLUGIN_BASE @@ -391,6 +393,8 @@ class PanelMixinTests(TestCase): # Enable the 'broken panel' setting - this will cause all panels to not render plugin.set_setting('ENABLE_BROKEN_PANEL', True) + n_errors = Error.objects.count() + for url in urls: response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -400,3 +404,6 @@ class PanelMixinTests(TestCase): self.assertNotIn('Hello world!', str(response.content)) self.assertNotIn('Broken Panel', str(response.content)) self.assertNotIn('Custom Part Panel', str(response.content)) + + # Assert that each request threw an error + self.assertEqual(Error.objects.count(), n_errors + len(urls)) From e93b138de4f5e1e38a1b447b8a4e088bdf0614e5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 19 May 2022 19:49:39 +1000 Subject: [PATCH 71/72] Expand unit tests for settings - Check that name and description are translated - Check for only allowed keys - Extend checks to include user settings - Check default value for boolean setting --- InvenTree/common/tests.py | 69 +++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 8e3f69c21e..7f6f6dbe40 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -112,28 +112,61 @@ class SettingsTest(TestCase): self.assertIn('STOCK_OWNERSHIP_CONTROL', result) self.assertIn('SIGNUP_GROUP', result) - def test_required_values(self): + def run_settings_check(self, key, setting): + + self.assertTrue(type(setting) is dict) + + name = setting.get('name', None) + + self.assertIsNotNone(name) + self.assertIn('django.utils.functional.lazy', str(type(name))) + + description = setting.get('description', None) + + self.assertIsNotNone(description) + self.assertIn('django.utils.functional.lazy', str(type(description))) + + if key != key.upper(): + raise ValueError(f"Setting key '{key}' is not uppercase") # pragma: no cover + + # Check that only allowed keys are provided + allowed_keys = [ + 'name', + 'description', + 'default', + 'validator', + 'hidden', + 'choices', + 'units', + 'requires_restart', + ] + + for k in setting.keys(): + self.assertIn(k, allowed_keys) + + # Check default value for boolean settings + validator = setting.get('validator', None) + + if validator is bool: + default = setting.get('default', None) + + # Default value *must* be supplied for boolean setting! + self.assertIsNotNone(default) + + # Default value for boolean must itself be a boolean + self.assertIn(default, [True, False]) + + def test_setting_data(self): """ - - Ensure that every global setting has a name. - - Ensure that every global setting has a description. + - Ensure that every setting has a name, which is translated + - Ensure that every setting has a description, which is translated """ - for key in InvenTreeSetting.SETTINGS.keys(): + for key, setting in InvenTreeSetting.SETTINGS.items(): + self.run_settings_check(key, setting) - setting = InvenTreeSetting.SETTINGS[key] - - name = setting.get('name', None) - - if name is None: - raise ValueError(f'Missing GLOBAL_SETTING name for {key}') # pragma: no cover - - description = setting.get('description', None) - - if description is None: - raise ValueError(f'Missing GLOBAL_SETTING description for {key}') # pragma: no cover - - if key != key.upper(): - raise ValueError(f"SETTINGS key '{key}' is not uppercase") # pragma: no cover + for key, setting in InvenTreeUserSetting.SETTINGS.items(): + self.run_settings_check(key, setting) def test_defaults(self): """ From d9beaab92c1b1799bbc8c39e0243802f62f212fc Mon Sep 17 00:00:00 2001 From: Maksim Stojkovic <18454392+maksimstojkovic@users.noreply.github.com> Date: Thu, 19 May 2022 20:31:55 +1000 Subject: [PATCH 72/72] Primitive solution to SO reload on shipment --- InvenTree/templates/js/translated/order.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index e2bee865fd..53dead4b60 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -138,7 +138,8 @@ function completeShipment(shipment_id) { $('#so-lines-table').bootstrapTable('refresh'); $('#pending-shipments-table').bootstrapTable('refresh'); $('#completed-shipments-table').bootstrapTable('refresh'); - } + }, + reload: true }); } });