From 0e157950dce2e519681f13104f87c449ee3d28c4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Jul 2022 08:19:49 +1000 Subject: [PATCH 1/5] Update unit tests for API plugin mixin class (#3328) * Update unit tests for API plugin mixin class - API at previous target URL has changed - Simplier to use the github API as a test case * Revert test database name * Override default URL behaviour for unit test --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/plugin/base/integration/mixins.py | 5 +++ .../plugin/base/integration/test_mixins.py | 34 +++++++++++++------ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 6d62f1302d..e19896a7a6 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -551,7 +551,7 @@ if "mysql" in db_engine: # pragma: no cover # https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level if "isolation_level" not in db_options: serializable = _is_true( - os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true") + os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "false") ) db_options["isolation_level"] = ( "serializable" if serializable else "read committed" diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index b4565e0dbc..7a14a9a890 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -443,6 +443,10 @@ class APICallMixin: if endpoint_is_url: url = endpoint else: + + if endpoint.startswith('/'): + endpoint = endpoint[1:] + url = f'{self.api_url}/{endpoint}' # build kwargs for call @@ -450,6 +454,7 @@ class APICallMixin: 'url': url, 'headers': headers, } + if data: kwargs['data'] = json.dumps(data) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 40cb1591b8..13cfb47d79 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -173,6 +173,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): def setUp(self): """Setup for all tests.""" + class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin): NAME = "Sample API Caller" @@ -184,23 +185,32 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): 'API_URL': { 'name': 'External URL', 'description': 'Where is your API located?', - 'default': 'reqres.in', + 'default': 'https://api.github.com', }, } + API_URL_SETTING = 'API_URL' API_TOKEN_SETTING = 'API_TOKEN' + @property + def api_url(self): + """Override API URL for this test""" + return "https://api.github.com" + def get_external_url(self, simple: bool = True): """Returns data from the sample endpoint.""" - return self.api_call('api/users/2', simple_response=simple) + return self.api_call('orgs/inventree', simple_response=simple) + self.mixin = MixinCls() class WrongCLS(APICallMixin, InvenTreePlugin): pass + self.mixin_wrong = WrongCLS() class WrongCLS2(APICallMixin, InvenTreePlugin): API_URL_SETTING = 'test' + self.mixin_wrong2 = WrongCLS2() def test_base_setup(self): @@ -208,7 +218,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # check init self.assertTrue(self.mixin.has_api_call) # api_url - self.assertEqual('https://reqres.in', self.mixin.api_url) + self.assertEqual('https://api.github.com', self.mixin.api_url) # api_headers headers = self.mixin.api_headers @@ -232,7 +242,9 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # api_call result = self.mixin.get_external_url() self.assertTrue(result) - self.assertIn('data', result,) + + for key in ['login', 'email', 'name', 'twitter_username']: + self.assertIn(key, result) # api_call without json conversion result = self.mixin.get_external_url(False) @@ -240,22 +252,22 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(result.reason, 'OK') # api_call with full url - result = self.mixin.api_call('https://reqres.in/api/users/2', endpoint_is_url=True) + result = self.mixin.api_call('https://api.github.com/orgs/inventree', endpoint_is_url=True) self.assertTrue(result) # api_call with post and data result = self.mixin.api_call( - 'api/users/', - data={"name": "morpheus", "job": "leader"}, - method='POST' + 'repos/inventree/InvenTree', + method='GET' ) + self.assertTrue(result) - self.assertEqual(result['name'], 'morpheus') + self.assertEqual(result['name'], 'InvenTree') + self.assertEqual(result['html_url'], 'https://github.com/inventree/InvenTree') # api_call with filter - result = self.mixin.api_call('api/users', url_args={'page': '2'}) + result = self.mixin.api_call('repos/inventree/InvenTree/stargazers', url_args={'page': '2'}) self.assertTrue(result) - self.assertEqual(result['page'], 2) def test_function_errors(self): """Test function errors.""" From 2635327c51cc1d58eb5b8a1975e5f24542e70a83 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Jul 2022 11:55:39 +1000 Subject: [PATCH 2/5] Add 'refresh' button for part parameter table (#3329) * Add 'refresh' button for part parameter table * Override default URL behaviour for unit test (cherry picked from commit 2c12a695294c2785e82b7f469f79a7d1a5412e71) --- InvenTree/part/templates/part/category.html | 6 ++++++ InvenTree/templates/js/translated/part.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 49e49f2931..d31fe08321 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -206,6 +206,12 @@

{% trans "Part Parameters" %}

+
+
+ {% include "filter_list.html" with id="parameters" %} +
+
+
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 46579493c4..8f541df2be 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -1198,6 +1198,8 @@ function loadRelatedPartsTable(table, part_id, options={}) { */ function loadParametricPartTable(table, options={}) { + setupFilterList('parameters', $(table), '#filter-list-parameters'); + var columns = [ { field: 'name', From 2afd39356af478042969f6e554be4e732cc23034 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Jul 2022 11:57:27 +1000 Subject: [PATCH 3/5] Int migration fix (#3323) * Add fix for stock migration - Ensure the serial number is not too large when performing migration - Add unit test for data migration (cherry picked from commit 661fbf0e3dbdf6444d3d25b02d68ad229925d87c) * Add similar fixes for PO and SO migrations (cherry picked from commit bde23c130c879e7663091fba808bbd57c52ed8bf) * And similar fix for BuildOrder reference field (cherry picked from commit ca0f4e00310aed0551f8fad5c57f90fae2177f04) * Update unit tests for API plugin mixin class - API at previous target URL has changed - Simplier to use the github API as a test case (cherry picked from commit dfe3172b7d7e7c6910aff0e6248b5609570607a9) * Revert test database name (cherry picked from commit 53333c29c38ae393b1e31e764e08a1239839a594) * Override default URL behaviour for unit test (cherry picked from commit 2c12a695294c2785e82b7f469f79a7d1a5412e71) --- .../migrations/0032_auto_20211014_0632.py | 4 ++ .../migrations/0052_auto_20211014_0631.py | 8 +++ InvenTree/order/test_migrations.py | 22 ++++++ .../migrations/0069_auto_20211109_2347.py | 3 + InvenTree/stock/test_migrations.py | 69 +++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 InvenTree/stock/test_migrations.py diff --git a/InvenTree/build/migrations/0032_auto_20211014_0632.py b/InvenTree/build/migrations/0032_auto_20211014_0632.py index ea6d5c954d..1ae56af4e6 100644 --- a/InvenTree/build/migrations/0032_auto_20211014_0632.py +++ b/InvenTree/build/migrations/0032_auto_20211014_0632.py @@ -24,6 +24,10 @@ def build_refs(apps, schema_editor): except Exception: # pragma: no cover ref = 0 + # Clip integer value to ensure it does not overflow database field + if ref > 0x7fffffff: + ref = 0x7fffffff + build.reference_int = ref build.save() diff --git a/InvenTree/order/migrations/0052_auto_20211014_0631.py b/InvenTree/order/migrations/0052_auto_20211014_0631.py index 94a591d3d6..36e2d882ac 100644 --- a/InvenTree/order/migrations/0052_auto_20211014_0631.py +++ b/InvenTree/order/migrations/0052_auto_20211014_0631.py @@ -23,6 +23,10 @@ def build_refs(apps, schema_editor): except Exception: # pragma: no cover ref = 0 + # Clip integer value to ensure it does not overflow database field + if ref > 0x7fffffff: + ref = 0x7fffffff + order.reference_int = ref order.save() @@ -40,6 +44,10 @@ def build_refs(apps, schema_editor): except Exception: # pragma: no cover ref = 0 + # Clip integer value to ensure it does not overflow database field + if ref > 0x7fffffff: + ref = 0x7fffffff + order.reference_int = ref order.save() diff --git a/InvenTree/order/test_migrations.py b/InvenTree/order/test_migrations.py index 1734501a9c..a45f09fc48 100644 --- a/InvenTree/order/test_migrations.py +++ b/InvenTree/order/test_migrations.py @@ -49,6 +49,19 @@ class TestRefIntMigrations(MigratorTestCase): with self.assertRaises(AttributeError): print(sales_order.reference_int) + # Create orders with very large reference values + self.po_pk = PurchaseOrder.objects.create( + supplier=supplier, + reference='999999999999999999999999999999999', + description='Big reference field', + ).pk + + self.so_pk = SalesOrder.objects.create( + customer=supplier, + reference='999999999999999999999999999999999', + description='Big reference field', + ).pk + def test_ref_field(self): """Test that the 'reference_int' field has been created and is filled out correctly.""" PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder') @@ -63,6 +76,15 @@ class TestRefIntMigrations(MigratorTestCase): self.assertEqual(po.reference_int, ii) self.assertEqual(so.reference_int, ii) + # Tests for orders with overly large reference values + po = PurchaseOrder.objects.get(pk=self.po_pk) + self.assertEqual(po.reference, '999999999999999999999999999999999') + self.assertEqual(po.reference_int, 0x7fffffff) + + so = SalesOrder.objects.get(pk=self.so_pk) + self.assertEqual(so.reference, '999999999999999999999999999999999') + self.assertEqual(so.reference_int, 0x7fffffff) + class TestShipmentMigration(MigratorTestCase): """Test data migration for the "SalesOrderShipment" model.""" diff --git a/InvenTree/stock/migrations/0069_auto_20211109_2347.py b/InvenTree/stock/migrations/0069_auto_20211109_2347.py index e4e8128f1c..2691b6305e 100644 --- a/InvenTree/stock/migrations/0069_auto_20211109_2347.py +++ b/InvenTree/stock/migrations/0069_auto_20211109_2347.py @@ -28,6 +28,9 @@ def update_serials(apps, schema_editor): except Exception: serial = 0 + # Ensure the integer value is not too large for the database field + if serial > 0x7fffffff: + serial = 0x7fffffff item.serial_int = serial item.save() diff --git a/InvenTree/stock/test_migrations.py b/InvenTree/stock/test_migrations.py new file mode 100644 index 0000000000..07fa9f2fc7 --- /dev/null +++ b/InvenTree/stock/test_migrations.py @@ -0,0 +1,69 @@ +"""Unit tests for data migrations in the 'stock' app""" + +from django_test_migrations.contrib.unittest_case import MigratorTestCase + +from InvenTree import helpers + + +class TestSerialNumberMigration(MigratorTestCase): + """Test data migration which updates serial numbers""" + + migrate_from = ('stock', '0067_alter_stockitem_part') + migrate_to = ('stock', helpers.getNewestMigrationFile('stock')) + + def prepare(self): + """Create initial data for this migration""" + + Part = self.old_state.apps.get_model('part', 'part') + StockItem = self.old_state.apps.get_model('stock', 'stockitem') + + # Create a base part + my_part = Part.objects.create( + name='PART-123', + description='Some part', + active=True, + trackable=True, + level=0, + tree_id=0, + lft=0, rght=0 + ) + + # Create some serialized stock items + for sn in range(10, 20): + StockItem.objects.create( + part=my_part, + quantity=1, + serial=sn, + level=0, + tree_id=0, + lft=0, rght=0 + ) + + # Create a stock item with a very large serial number + item = StockItem.objects.create( + part=my_part, + quantity=1, + serial='9999999999999999999999999999999999999999999999999999999999999', + level=0, + tree_id=0, + lft=0, rght=0 + ) + + self.big_ref_pk = item.pk + + def test_migrations(self): + """Test that the migrations have been applied correctly""" + + StockItem = self.new_state.apps.get_model('stock', 'stockitem') + + # Check that the serial number integer conversion has been applied correctly + for sn in range(10, 20): + item = StockItem.objects.get(serial_int=sn) + + self.assertEqual(item.serial, str(sn)) + + big_ref_item = StockItem.objects.get(pk=self.big_ref_pk) + + # Check that the StockItem maximum serial number + self.assertEqual(big_ref_item.serial, '9999999999999999999999999999999999999999999999999999999999999') + self.assertEqual(big_ref_item.serial_int, 0x7fffffff) From 739489840be6605f9856fb6debd5887ba4122288 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Jul 2022 15:16:35 +1000 Subject: [PATCH 4/5] Support CNY by default (#3334) - Adds default support for CNY currency --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/config_template.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index e19896a7a6..133474571e 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -744,7 +744,7 @@ if get_setting('TEST_TRANSLATIONS', False): # pragma: no cover CURRENCIES = CONFIG.get( 'currencies', [ - 'AUD', 'CAD', 'EUR', 'GBP', 'JPY', 'NZD', 'USD', + 'AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'NZD', 'USD', ], ) diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index af975e7267..a470ac62e2 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -61,6 +61,7 @@ base_currency: USD currencies: - AUD - CAD + - CNY - EUR - GBP - JPY From 653dcd4526309842d213559029f9e95e2d6b1190 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Jul 2022 17:37:02 +1000 Subject: [PATCH 5/5] Annotate "in_stock" quantity to SupplierPart API (#3335) * Annotate "in_stock" quantity to SupplierPart API * Increment API version --- InvenTree/InvenTree/api_version.py | 5 ++++- InvenTree/company/api.py | 7 +++++++ InvenTree/company/serializers.py | 19 +++++++++++++++++++ InvenTree/templates/js/translated/company.js | 6 ++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index c5e937b10b..0d3fd7bf69 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 64 +INVENTREE_API_VERSION = 65 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v65 -> 2022-07-15 : https://github.com/inventree/InvenTree/pull/3335 + - Annotates 'in_stock' quantity to the SupplierPart API + v64 -> 2022-07-08 : https://github.com/inventree/InvenTree/pull/3310 - Annotate 'on_order' quantity to BOM list API - Allow BOM List API endpoint to be filtered by "on_order" parameter diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 2f1e1ba421..734dd08b5e 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -263,6 +263,13 @@ class SupplierPartList(ListCreateDestroyAPIView): queryset = SupplierPart.objects.all() + def get_queryset(self, *args, **kwargs): + """Return annotated queryest object for the SupplierPart list""" + queryset = super().get_queryset(*args, **kwargs) + queryset = SupplierPartSerializer.annotate_queryset(queryset) + + return queryset + def filter_queryset(self, queryset): """Custom filtering for the queryset.""" queryset = super().filter_queryset(queryset) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 08947605ab..7da90f47a5 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from sql_util.utils import SubqueryCount +import part.filters from common.settings import currency_code_default, currency_code_mappings from InvenTree.serializers import (InvenTreeAttachmentSerializer, InvenTreeDecimalField, @@ -199,6 +200,9 @@ class ManufacturerPartParameterSerializer(InvenTreeModelSerializer): class SupplierPartSerializer(InvenTreeModelSerializer): """Serializer for SupplierPart object.""" + # Annotated field showing total in-stock quantity + in_stock = serializers.FloatField(read_only=True) + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) @@ -249,6 +253,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer): 'available', 'availability_updated', 'description', + 'in_stock', 'link', 'manufacturer', 'manufacturer_detail', @@ -270,6 +275,20 @@ class SupplierPartSerializer(InvenTreeModelSerializer): 'availability_updated', ] + @staticmethod + def annotate_queryset(queryset): + """Annotate the SupplierPart queryset with extra fields: + + Fields: + in_stock: Current stock quantity for each SupplierPart + """ + + queryset = queryset.annotate( + in_stock=part.filters.annotate_total_stock(reference='part__') + ) + + return queryset + def update(self, supplier_part, data): """Custom update functionality for the serializer""" diff --git a/InvenTree/templates/js/translated/company.js b/InvenTree/templates/js/translated/company.js index 8a167f5c20..5af324d32f 100644 --- a/InvenTree/templates/js/translated/company.js +++ b/InvenTree/templates/js/translated/company.js @@ -840,6 +840,7 @@ function loadSupplierPartTable(table, url, options) { queryParams: filters, name: 'supplierparts', groupBy: false, + sortable: true, formatNoMatches: function() { return '{% trans "No supplier parts found" %}'; }, @@ -957,6 +958,11 @@ function loadSupplierPartTable(table, url, options) { title: '{% trans "Packaging" %}', sortable: false, }, + { + field: 'in_stock', + title: '{% trans "In Stock" %}', + sortable: true, + }, { field: 'available', title: '{% trans "Available" %}',