2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 11:35:41 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters
2022-07-15 17:42:51 +10:00
15 changed files with 181 additions and 14 deletions

View File

@ -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

View File

@ -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',
],
)

View File

@ -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()

View File

@ -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)

View File

@ -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"""

View File

@ -61,6 +61,7 @@ base_currency: USD
currencies:
- AUD
- CAD
- CNY
- EUR
- GBP
- JPY

View File

@ -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()

View File

@ -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."""

View File

@ -206,6 +206,12 @@
<h4>{% trans "Part Parameters" %}</h4>
</div>
<div class='panel-content'>
<div id='param-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="parameters" %}
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#param-button-toolbar' id='parametric-part-table'>
</table>
</div>

View File

@ -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)

View File

@ -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."""

View File

@ -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()

View File

@ -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)

View File

@ -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" %}',

View File

@ -1198,6 +1198,8 @@ function loadRelatedPartsTable(table, part_id, options={}) {
*/
function loadParametricPartTable(table, options={}) {
setupFilterList('parameters', $(table), '#filter-list-parameters');
var columns = [
{
field: 'name',