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/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 6d62f1302d..133474571e 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"
@@ -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/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/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/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
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/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" %}
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."""
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)
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" %}',
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',