mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 21:38:48 +00:00
A bit more testing (#8053)
* add more admin testing * fix assertations * add test for importer admin * Add tests for https://github.com/inventree/InvenTree/pull/7164 * add common/attachment test * fix test * add tests * remove unused definition - the view is read only * Revert "remove unused definition - the view is read only" This reverts commit 4cad8d16f395c447acffc241d90757cdf72109c0. * more tests in report * Update tests.py * make lookup dynamic * make report assertation dynamic * add migration test * extend validation plugin tests * disable flaky test * Add test for barcode/uid transition * test reverse migration * cleanup new test * remove empty action * split and refactor API tests * refactor test * Add test for error conditions * fix double entry * more migration tests * also test no history * fix assertation * add another migration test * fix typo * fix manufacturer filter * test more filters * even more filter tests * move top level test to right place * add todos for tests that could be more expressive * add test for checking duplicate serials * ignore cautious catches
This commit is contained in:
parent
019b08af3f
commit
96a2517402
@ -23,7 +23,12 @@ import PIL
|
|||||||
import common.validators
|
import common.validators
|
||||||
from common.settings import get_global_setting, set_global_setting
|
from common.settings import get_global_setting, set_global_setting
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase, PluginMixin
|
from InvenTree.unit_test import (
|
||||||
|
AdminTestCase,
|
||||||
|
InvenTreeAPITestCase,
|
||||||
|
InvenTreeTestCase,
|
||||||
|
PluginMixin,
|
||||||
|
)
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
from plugin.models import NotificationUserSetting
|
from plugin.models import NotificationUserSetting
|
||||||
@ -1676,3 +1681,14 @@ class CustomStatusTest(TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
instance.__str__(), 'Stock Item (StockStatus): OK - advanced | 11 (10)'
|
instance.__str__(), 'Stock Item (StockStatus): OK - advanced | 11 (10)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminTest(AdminTestCase):
|
||||||
|
"""Tests for the admin interface integration."""
|
||||||
|
|
||||||
|
def test_admin(self):
|
||||||
|
"""Test the admin URL."""
|
||||||
|
self.helper(
|
||||||
|
model=Attachment,
|
||||||
|
model_kwargs={'link': 'https://aa.example.org', 'model_id': 1},
|
||||||
|
)
|
||||||
|
@ -39,7 +39,6 @@ __all__ = [
|
|||||||
'SingleNotificationMethod',
|
'SingleNotificationMethod',
|
||||||
'SupplierBarcodeMixin',
|
'SupplierBarcodeMixin',
|
||||||
'UrlsMixin',
|
'UrlsMixin',
|
||||||
'UrlsMixin',
|
|
||||||
'UserInterfaceMixin',
|
'UserInterfaceMixin',
|
||||||
'ValidationMixin',
|
'ValidationMixin',
|
||||||
]
|
]
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
"""Unit tests for the SampleValidatorPlugin class."""
|
"""Unit tests for the SampleValidatorPlugin class."""
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
import build.models
|
||||||
import part.models
|
import part.models
|
||||||
from InvenTree.unit_test import InvenTreeTestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
|
|
||||||
class SampleValidatorPluginTest(InvenTreeTestCase):
|
class SampleValidatorPluginTest(InvenTreeAPITestCase, InvenTreeTestCase):
|
||||||
"""Tests for the SampleValidatonPlugin class."""
|
"""Tests for the SampleValidatonPlugin class."""
|
||||||
|
|
||||||
fixtures = ['category', 'location']
|
fixtures = ['part', 'category', 'location', 'build']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up the test environment."""
|
"""Set up the test environment."""
|
||||||
@ -28,6 +30,7 @@ class SampleValidatorPluginTest(InvenTreeTestCase):
|
|||||||
self.bom_item = part.models.BomItem.objects.create(
|
self.bom_item = part.models.BomItem.objects.create(
|
||||||
part=self.assembly, sub_part=self.part, quantity=1
|
part=self.assembly, sub_part=self.part, quantity=1
|
||||||
)
|
)
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
def get_plugin(self):
|
def get_plugin(self):
|
||||||
"""Return the SampleValidatorPlugin instance."""
|
"""Return the SampleValidatorPlugin instance."""
|
||||||
@ -113,3 +116,40 @@ class SampleValidatorPluginTest(InvenTreeTestCase):
|
|||||||
self.part.IPN = 'LMNOPQ'
|
self.part.IPN = 'LMNOPQ'
|
||||||
|
|
||||||
self.part.save()
|
self.part.save()
|
||||||
|
|
||||||
|
def test_validate_generate_batch_code(self):
|
||||||
|
"""Test the generate_batch_code function."""
|
||||||
|
self.enable_plugin(True)
|
||||||
|
plg = self.get_plugin()
|
||||||
|
self.assertIsNotNone(plg)
|
||||||
|
|
||||||
|
code = plg.generate_batch_code()
|
||||||
|
self.assertIsInstance(code, str)
|
||||||
|
self.assertTrue(code.startswith('SAMPLE-BATCH'))
|
||||||
|
|
||||||
|
def test_api_batch(self):
|
||||||
|
"""Test the batch code validation API."""
|
||||||
|
self.enable_plugin(True)
|
||||||
|
url = reverse('api-generate-batch-code')
|
||||||
|
|
||||||
|
response = self.post(url)
|
||||||
|
self.assertIn('batch_code', response.data)
|
||||||
|
self.assertTrue(response.data['batch_code'].startswith('SAMPLE-BATCH'))
|
||||||
|
|
||||||
|
# Use part code
|
||||||
|
part_itm = part.models.Part.objects.first()
|
||||||
|
response = self.post(url, {'part': part_itm.pk})
|
||||||
|
self.assertIn('batch_code', response.data)
|
||||||
|
self.assertTrue(
|
||||||
|
response.data['batch_code'].startswith(part_itm.name + '-SAMPLE-BATCH')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use build_order
|
||||||
|
build_itm = build.models.Build.objects.first()
|
||||||
|
response = self.post(url, {'build_order': build_itm.pk})
|
||||||
|
self.assertIn('batch_code', response.data)
|
||||||
|
self.assertTrue(
|
||||||
|
response.data['batch_code'].startswith(
|
||||||
|
build_itm.reference + '-SAMPLE-BATCH'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -18,6 +18,7 @@ from build.models import Build
|
|||||||
from common.models import Attachment, InvenTreeSetting
|
from common.models import Attachment, InvenTreeSetting
|
||||||
from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase
|
from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase
|
||||||
from order.models import ReturnOrder, SalesOrder
|
from order.models import ReturnOrder, SalesOrder
|
||||||
|
from part.models import Part
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
from report.models import LabelTemplate, ReportTemplate
|
from report.models import LabelTemplate, ReportTemplate
|
||||||
from report.templatetags import barcode as barcode_tags
|
from report.templatetags import barcode as barcode_tags
|
||||||
@ -305,6 +306,16 @@ class ReportTest(InvenTreeAPITestCase):
|
|||||||
response = self.get(url, {'enabled': False})
|
response = self.get(url, {'enabled': False})
|
||||||
self.assertEqual(len(response.data), n)
|
self.assertEqual(len(response.data), n)
|
||||||
|
|
||||||
|
# Filter by items
|
||||||
|
part_pk = Part.objects.first().pk
|
||||||
|
report = ReportTemplate.objects.filter(model_type='part').first()
|
||||||
|
return
|
||||||
|
# TODO @matmair re-enable this (in GitHub Actions) flaky test
|
||||||
|
response = self.get(url, {'model_type': 'part', 'items': part_pk})
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertEqual(response.data[0]['pk'], report.pk)
|
||||||
|
self.assertEqual(response.data[0]['name'], report.name)
|
||||||
|
|
||||||
def test_create_endpoint(self):
|
def test_create_endpoint(self):
|
||||||
"""Test that creating a new report works for each report."""
|
"""Test that creating a new report works for each report."""
|
||||||
url = reverse('api-report-template-list')
|
url = reverse('api-report-template-list')
|
||||||
@ -533,6 +544,22 @@ class PrintTestMixins:
|
|||||||
max_query_count=500 * len(qs),
|
max_query_count=500 * len(qs),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test with wrong dimensions
|
||||||
|
if not hasattr(template, 'width'):
|
||||||
|
return
|
||||||
|
|
||||||
|
org_width = template.width
|
||||||
|
template.width = 0
|
||||||
|
template.save()
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
{'template': template.pk, 'plugin': plugin.pk, 'items': [qs[0].pk]},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
self.assertEqual(str(response.data['template'][0]), 'Invalid label dimensions')
|
||||||
|
template.width = org_width
|
||||||
|
template.save()
|
||||||
|
|
||||||
|
|
||||||
class TestReportTest(PrintTestMixins, ReportTest):
|
class TestReportTest(PrintTestMixins, ReportTest):
|
||||||
"""Unit testing class for the stock item TestReport model."""
|
"""Unit testing class for the stock item TestReport model."""
|
||||||
|
@ -144,7 +144,7 @@ class StockDetail(RetrieveUpdateDestroyAPI):
|
|||||||
params.get('supplier_part_detail', True)
|
params.get('supplier_part_detail', True)
|
||||||
)
|
)
|
||||||
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
||||||
except AttributeError:
|
except AttributeError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
@ -164,7 +164,7 @@ class StockItemContextMixin:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
context['item'] = StockItem.objects.get(pk=self.kwargs.get('pk', None))
|
context['item'] = StockItem.objects.get(pk=self.kwargs.get('pk', None))
|
||||||
except Exception:
|
except Exception: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -526,7 +526,8 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
def filter_manufacturer(self, queryset, name, company):
|
def filter_manufacturer(self, queryset, name, company):
|
||||||
"""Filter by manufacturer."""
|
"""Filter by manufacturer."""
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(is_manufacturer=True) & Q(manufacturer_part__manufacturer=company)
|
Q(supplier_part__manufacturer_part__manufacturer__is_manufacturer=True)
|
||||||
|
& Q(supplier_part__manufacturer_part__manufacturer=company)
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier = rest_filters.ModelChoiceFilter(
|
supplier = rest_filters.ModelChoiceFilter(
|
||||||
@ -891,7 +892,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|||||||
'tests',
|
'tests',
|
||||||
]:
|
]:
|
||||||
kwargs[key] = str2bool(params.get(key, False))
|
kwargs[key] = str2bool(params.get(key, False))
|
||||||
except AttributeError:
|
except AttributeError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
@ -982,7 +983,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|||||||
data['purchase_price'] = float(
|
data['purchase_price'] = float(
|
||||||
data['purchase_price']
|
data['purchase_price']
|
||||||
) / float(supplier_part.pack_quantity_native)
|
) / float(supplier_part.pack_quantity_native)
|
||||||
except ValueError:
|
except ValueError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Now remove the flag from data, so that it doesn't interfere with saving
|
# Now remove the flag from data, so that it doesn't interfere with saving
|
||||||
@ -1096,7 +1097,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|||||||
pk__in=[it.pk for it in item.get_descendants(include_self=True)]
|
pk__in=[it.pk for it in item.get_descendants(include_self=True)]
|
||||||
)
|
)
|
||||||
|
|
||||||
except (ValueError, StockItem.DoesNotExist):
|
except (ValueError, StockItem.DoesNotExist): # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Exclude StockItems which are already allocated to a particular SalesOrder
|
# Exclude StockItems which are already allocated to a particular SalesOrder
|
||||||
@ -1114,7 +1115,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|||||||
# Exclude any stock item which is already allocated to the sales order
|
# Exclude any stock item which is already allocated to the sales order
|
||||||
queryset = queryset.exclude(pk__in=[a.item.pk for a in allocations])
|
queryset = queryset.exclude(pk__in=[a.item.pk for a in allocations])
|
||||||
|
|
||||||
except (ValueError, SalesOrder.DoesNotExist):
|
except (ValueError, SalesOrder.DoesNotExist): # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Does the client wish to filter by the Part ID?
|
# Does the client wish to filter by the Part ID?
|
||||||
@ -1160,7 +1161,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|||||||
else:
|
else:
|
||||||
queryset = queryset.filter(location=loc_id)
|
queryset = queryset.filter(location=loc_id)
|
||||||
|
|
||||||
except (ValueError, StockLocation.DoesNotExist):
|
except (ValueError, StockLocation.DoesNotExist): # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -1223,7 +1224,7 @@ class StockItemTestResultMixin:
|
|||||||
kwargs['template_detail'] = str2bool(
|
kwargs['template_detail'] = str2bool(
|
||||||
self.request.query_params.get('template_detail', False)
|
self.request.query_params.get('template_detail', False)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
@ -1363,7 +1364,7 @@ class StockItemTestResultList(StockItemTestResultMixin, ListCreateDestroyAPIView
|
|||||||
|
|
||||||
queryset = queryset.filter(stock_item__in=items)
|
queryset = queryset.filter(stock_item__in=items)
|
||||||
|
|
||||||
except (ValueError, StockItem.DoesNotExist):
|
except (ValueError, StockItem.DoesNotExist): # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -1405,14 +1406,14 @@ class StockTrackingList(DataExportViewMixin, ListAPI):
|
|||||||
kwargs['item_detail'] = str2bool(
|
kwargs['item_detail'] = str2bool(
|
||||||
self.request.query_params.get('item_detail', False)
|
self.request.query_params.get('item_detail', False)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kwargs['user_detail'] = str2bool(
|
kwargs['user_detail'] = str2bool(
|
||||||
self.request.query_params.get('user_detail', False)
|
self.request.query_params.get('user_detail', False)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
tree_id: 2
|
tree_id: 2
|
||||||
lft: 1
|
lft: 1
|
||||||
rght: 8
|
rght: 8
|
||||||
|
external: True
|
||||||
|
|
||||||
- model: stock.stocklocation
|
- model: stock.stocklocation
|
||||||
pk: 5
|
pk: 5
|
||||||
|
@ -4,22 +4,6 @@ import InvenTree.fields
|
|||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
import djmoney.models.fields
|
import djmoney.models.fields
|
||||||
|
|
||||||
from django.db.migrations.recorder import MigrationRecorder
|
|
||||||
|
|
||||||
|
|
||||||
def show_migrations(apps, schema_editor):
|
|
||||||
"""Show the latest migrations from each app"""
|
|
||||||
|
|
||||||
for app in apps.get_app_configs():
|
|
||||||
|
|
||||||
label = app.label
|
|
||||||
|
|
||||||
migrations = MigrationRecorder.Migration.objects.filter(app=app).order_by('-applied')[:5]
|
|
||||||
|
|
||||||
print(f"{label} migrations:")
|
|
||||||
for m in migrations:
|
|
||||||
print(f" - {m.name}")
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
@ -30,10 +14,6 @@ class Migration(migrations.Migration):
|
|||||||
operations = []
|
operations = []
|
||||||
|
|
||||||
xoperations = [
|
xoperations = [
|
||||||
migrations.RunPython(
|
|
||||||
code=show_migrations,
|
|
||||||
reverse_code=migrations.RunPython.noop
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='stockitem',
|
model_name='stockitem',
|
||||||
name='purchase_price',
|
name='purchase_price',
|
||||||
|
@ -43,7 +43,7 @@ def update_templates(apps, schema_editor):
|
|||||||
|
|
||||||
# For each bad result, attempt to find a matching template
|
# For each bad result, attempt to find a matching template
|
||||||
# Here, a matching template must point to a part *above* the part in the tree
|
# Here, a matching template must point to a part *above* the part in the tree
|
||||||
# Annotate the queryset with a "mathching template"
|
# Annotate the queryset with a "matching template"
|
||||||
|
|
||||||
template_query = PartTestTemplate.objects.filter(
|
template_query = PartTestTemplate.objects.filter(
|
||||||
part__tree_id=OuterRef('stock_item__part__tree_id'),
|
part__tree_id=OuterRef('stock_item__part__tree_id'),
|
||||||
|
@ -19,6 +19,7 @@ import build.models
|
|||||||
import company.models
|
import company.models
|
||||||
import part.models
|
import part.models
|
||||||
from common.models import InvenTreeCustomUserStateModel, InvenTreeSetting
|
from common.models import InvenTreeCustomUserStateModel, InvenTreeSetting
|
||||||
|
from common.settings import set_global_setting
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from part.models import Part, PartTestTemplate
|
from part.models import Part, PartTestTemplate
|
||||||
from stock.models import (
|
from stock.models import (
|
||||||
@ -437,6 +438,12 @@ class StockLocationTest(StockAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(res), 1)
|
self.assertEqual(len(res), 1)
|
||||||
|
|
||||||
|
# top_level
|
||||||
|
res = self.get(
|
||||||
|
self.list_url, {'top_level': True, 'cascade': False}, expected_code=200
|
||||||
|
).json()
|
||||||
|
self.assertEqual(len(res), 4)
|
||||||
|
|
||||||
def test_stock_location_tree(self):
|
def test_stock_location_tree(self):
|
||||||
"""Test the StockLocationTree API endpoint."""
|
"""Test the StockLocationTree API endpoint."""
|
||||||
# Create a number of new locations
|
# Create a number of new locations
|
||||||
@ -586,6 +593,11 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(response), 29)
|
self.assertEqual(len(response), 29)
|
||||||
|
|
||||||
|
def test_filter_manufacturer(self):
|
||||||
|
"""Filter StockItem by manufacturer."""
|
||||||
|
response = self.get_stock(manufacturer='6')
|
||||||
|
self.assertEqual(len(response), 0)
|
||||||
|
|
||||||
def test_filter_by_part(self):
|
def test_filter_by_part(self):
|
||||||
"""Filter StockItem by Part reference."""
|
"""Filter StockItem by Part reference."""
|
||||||
response = self.get_stock(part=25)
|
response = self.get_stock(part=25)
|
||||||
@ -745,6 +757,79 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
response = self.get_stock(expired=0)
|
response = self.get_stock(expired=0)
|
||||||
self.assertEqual(len(response), 25)
|
self.assertEqual(len(response), 25)
|
||||||
|
|
||||||
|
def test_filter_external(self):
|
||||||
|
"""Filter StockItem by external."""
|
||||||
|
response = self.get_stock(external=True)
|
||||||
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
|
response = self.get_stock(external=False)
|
||||||
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
|
def test_filter_available(self):
|
||||||
|
"""Filter StockItem by available."""
|
||||||
|
response = self.get_stock(available=True)
|
||||||
|
self.assertEqual(len(response), 26)
|
||||||
|
|
||||||
|
response = self.get_stock(available=False)
|
||||||
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
|
def test_filter_installed(self):
|
||||||
|
"""Filter StockItem by installed."""
|
||||||
|
response = self.get_stock(installed=True)
|
||||||
|
self.assertEqual(len(response), 0)
|
||||||
|
|
||||||
|
response = self.get_stock(installed=False)
|
||||||
|
self.assertEqual(len(response), 29) # TODO: adjust test dataset (belongs_to)
|
||||||
|
|
||||||
|
def test_filter_has_installed(self):
|
||||||
|
"""Filter StockItem by has_installed."""
|
||||||
|
response = self.get_stock(has_installed_items=True)
|
||||||
|
self.assertEqual(len(response), 0)
|
||||||
|
|
||||||
|
response = self.get_stock(has_installed_items=False)
|
||||||
|
self.assertEqual(len(response), 29) # TODO: adjust test dataset (belongs_to)
|
||||||
|
|
||||||
|
def test_filter_has_child_items(self):
|
||||||
|
"""Filter StockItem by has_child_items."""
|
||||||
|
response = self.get_stock(has_child_items=True)
|
||||||
|
self.assertEqual(len(response), 0)
|
||||||
|
|
||||||
|
response = self.get_stock(has_child_items=False)
|
||||||
|
self.assertEqual(len(response), 29) # TODO: adjust test dataset (belongs_to)
|
||||||
|
|
||||||
|
def test_filter_sent_to_customer(self):
|
||||||
|
"""Filter StockItem by sent_to_customer."""
|
||||||
|
response = self.get_stock(sent_to_customer=True)
|
||||||
|
self.assertEqual(len(response), 0)
|
||||||
|
|
||||||
|
response = self.get_stock(sent_to_customer=False)
|
||||||
|
self.assertEqual(len(response), 29) # TODO: adjust test dataset
|
||||||
|
|
||||||
|
def test_filter_has_purchase_price(self):
|
||||||
|
"""Filter StockItem by has_purchase_price."""
|
||||||
|
response = self.get_stock(has_purchase_price=True)
|
||||||
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
|
response = self.get_stock(has_purchase_price=False)
|
||||||
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
|
def test_filter_stale(self):
|
||||||
|
"""Filter StockItem by stale."""
|
||||||
|
response = self.get_stock(stale=True)
|
||||||
|
self.assertEqual(len(response), 29)
|
||||||
|
|
||||||
|
response = self.get_stock(stale=False)
|
||||||
|
self.assertEqual(len(response), 29)
|
||||||
|
|
||||||
|
# Enable the 'stale' feature
|
||||||
|
set_global_setting('STOCK_STALE_DAYS', '10')
|
||||||
|
response = self.get_stock(stale=True)
|
||||||
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
|
response = self.get_stock(stale=False)
|
||||||
|
self.assertEqual(len(response), 28)
|
||||||
|
set_global_setting('STOCK_STALE_DAYS', '0')
|
||||||
|
|
||||||
def test_paginate(self):
|
def test_paginate(self):
|
||||||
"""Test that we can paginate results correctly."""
|
"""Test that we can paginate results correctly."""
|
||||||
for n in [1, 5, 10]:
|
for n in [1, 5, 10]:
|
||||||
@ -925,6 +1010,49 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.list_url, {'location_detail': True, 'tests': True}, max_query_count=35
|
self.list_url, {'location_detail': True, 'tests': True}, max_query_count=35
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_batch_generate_api(self):
|
||||||
|
"""Test helper API for batch management."""
|
||||||
|
set_global_setting(
|
||||||
|
'STOCK_BATCH_CODE_TEMPLATE', '{% if item %}{{ item.pk }}{% endif %}'
|
||||||
|
)
|
||||||
|
url = reverse('api-generate-batch-code')
|
||||||
|
|
||||||
|
response = self.post(url)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertIn('batch_code', response.data)
|
||||||
|
self.assertEqual(len(response.data['batch_code']), 0)
|
||||||
|
|
||||||
|
# With data
|
||||||
|
response = self.post(url, {'item': 1})
|
||||||
|
self.assertEqual(response.data['batch_code'], '1')
|
||||||
|
|
||||||
|
# With full data
|
||||||
|
response = self.post(url, {'item': 1, 'quantity': 2})
|
||||||
|
self.assertEqual(response.data['batch_code'], '1')
|
||||||
|
|
||||||
|
def test_serial_generate_api(self):
|
||||||
|
"""Test helper API for serial management."""
|
||||||
|
url = reverse('api-generate-serial-number')
|
||||||
|
|
||||||
|
# Generate serial number
|
||||||
|
response = self.post(url)
|
||||||
|
self.assertIn('serial_number', response.data)
|
||||||
|
|
||||||
|
# With full data
|
||||||
|
response = self.post(url, {'part': 1, 'quantity': 1})
|
||||||
|
self.assertEqual(response.data['serial_number'], '1001')
|
||||||
|
response = self.post(url, {'part': 1, 'quantity': 3})
|
||||||
|
self.assertEqual(response.data['serial_number'], '1001,1002,1003')
|
||||||
|
|
||||||
|
# Wrong quantities
|
||||||
|
response = self.post(url, {'part': 1, 'quantity': 'abc'}, expected_code=400)
|
||||||
|
self.assertEqual(response.data['quantity'], ['A valid integer is required.'])
|
||||||
|
|
||||||
|
response = self.post(url, {'part': 1, 'quantity': -2}, expected_code=400)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['quantity'], ['Quantity must be greater than zero']
|
||||||
|
)
|
||||||
|
|
||||||
def test_child_items(self):
|
def test_child_items(self):
|
||||||
"""Test that the 'child_items' annotation works as expected."""
|
"""Test that the 'child_items' annotation works as expected."""
|
||||||
# Create a trackable part
|
# Create a trackable part
|
||||||
@ -1298,6 +1426,18 @@ class StockItemTest(StockAPITestCase):
|
|||||||
self.assertEqual(trackable_part.stock_entries().count(), 10)
|
self.assertEqual(trackable_part.stock_entries().count(), 10)
|
||||||
self.assertEqual(trackable_part.get_stock_count(), 10)
|
self.assertEqual(trackable_part.get_stock_count(), 10)
|
||||||
|
|
||||||
|
# This should fail - wrong serial
|
||||||
|
response = self.post(
|
||||||
|
self.list_url,
|
||||||
|
data={'part': trackable_part.pk, 'quantity': 1, 'serial_numbers': '1'},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
'The following serial numbers already exist or are invalid : 1',
|
||||||
|
str(response.data),
|
||||||
|
)
|
||||||
|
self.assertEqual(trackable_part.get_stock_count(), 10)
|
||||||
|
|
||||||
def test_default_expiry(self):
|
def test_default_expiry(self):
|
||||||
"""Test that the "default_expiry" functionality works via the API.
|
"""Test that the "default_expiry" functionality works via the API.
|
||||||
|
|
||||||
@ -2259,3 +2399,38 @@ class StockMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
'api-stock-item-metadata': StockItem,
|
'api-stock-item-metadata': StockItem,
|
||||||
}.items():
|
}.items():
|
||||||
self.metatester(apikey, model)
|
self.metatester(apikey, model)
|
||||||
|
|
||||||
|
|
||||||
|
class StockStatisticsTest(StockAPITestCase):
|
||||||
|
"""Tests for the StockStatistics API endpoints."""
|
||||||
|
|
||||||
|
fixtures = [*StockAPITestCase.fixtures, 'build']
|
||||||
|
|
||||||
|
def test_test_statics(self):
|
||||||
|
"""Test the test statistics API endpoints."""
|
||||||
|
part = Part.objects.first()
|
||||||
|
response = self.get(
|
||||||
|
reverse('api-test-statistics-by-part', kwargs={'pk': part.pk}),
|
||||||
|
{},
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.data, [{}])
|
||||||
|
|
||||||
|
# Now trackable part
|
||||||
|
part1 = Part.objects.filter(trackable=True).first()
|
||||||
|
response = self.get(
|
||||||
|
reverse(
|
||||||
|
'api-test-statistics-by-part',
|
||||||
|
kwargs={'pk': part1.stock_items.first().pk},
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
expected_code=404,
|
||||||
|
)
|
||||||
|
self.assertIn('detail', response.data)
|
||||||
|
|
||||||
|
# 105
|
||||||
|
|
||||||
|
bld = build.models.Build.objects.first()
|
||||||
|
url = reverse('api-test-statistics-by-build', kwargs={'pk': bld.pk})
|
||||||
|
response = self.get(url, {}, expected_code=200)
|
||||||
|
self.assertEqual(response.data, [{}])
|
||||||
|
@ -231,3 +231,189 @@ class TestTestResultMigration(MigratorTestCase):
|
|||||||
|
|
||||||
for result in StockItemTestResult.objects.all():
|
for result in StockItemTestResult.objects.all():
|
||||||
self.assertIsNotNone(result.template)
|
self.assertIsNotNone(result.template)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPathstringMigration(MigratorTestCase):
|
||||||
|
"""Unit tests for StockLocation.Pathstring data migrations."""
|
||||||
|
|
||||||
|
migrate_from = ('stock', '0080_stocklocation_pathstring')
|
||||||
|
migrate_to = ('stock', '0081_auto_20220801_0044')
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
"""Create initial data."""
|
||||||
|
StockLocation = self.old_state.apps.get_model('stock', 'stocklocation')
|
||||||
|
|
||||||
|
# Create a test StockLocation
|
||||||
|
loc1 = StockLocation.objects.create(
|
||||||
|
name='Loc 1', level=0, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
loc2 = StockLocation.objects.create(
|
||||||
|
name='Loc 2', parent=loc1, level=1, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
StockLocation.objects.create(
|
||||||
|
name='Loc 3', parent=loc2, level=2, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
StockLocation.objects.create(name='Loc 4', level=0, lft=0, rght=0, tree_id=0)
|
||||||
|
|
||||||
|
# Check initial record counts
|
||||||
|
self.assertEqual(StockLocation.objects.count(), 4)
|
||||||
|
|
||||||
|
def test_migration(self):
|
||||||
|
"""Test that the migrations were applied as expected."""
|
||||||
|
StockLocation = self.old_state.apps.get_model('stock', 'stocklocation')
|
||||||
|
|
||||||
|
self.assertEqual(StockLocation.objects.count(), 4)
|
||||||
|
test_data = {
|
||||||
|
'Loc 1': 'Loc 1',
|
||||||
|
'Loc 2': 'Loc 1/Loc 2',
|
||||||
|
'Loc 3': 'Loc 1/Loc 2/Loc 3',
|
||||||
|
'Loc 4': 'Loc 4',
|
||||||
|
}
|
||||||
|
for loc_name, pathstring in test_data.items():
|
||||||
|
loc = StockLocation.objects.get(name=loc_name)
|
||||||
|
self.assertEqual(loc.pathstring, pathstring)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBarcodeToUidMigration(MigratorTestCase):
|
||||||
|
"""Unit tests for barcode to uid data migrations."""
|
||||||
|
|
||||||
|
migrate_from = ('stock', '0084_auto_20220903_0154')
|
||||||
|
migrate_to = ('stock', '0085_auto_20220903_0225')
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
"""Create initial data."""
|
||||||
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
|
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
|
||||||
|
|
||||||
|
# Create a test StockItem
|
||||||
|
part = Part.objects.create(name='test', level=0, lft=0, rght=0, tree_id=0)
|
||||||
|
StockItem.objects.create(
|
||||||
|
part_id=part.id, uid='12345', level=0, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
self.assertEqual(StockItem.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_migration(self):
|
||||||
|
"""Test that the migrations were applied as expected."""
|
||||||
|
StockItem = self.new_state.apps.get_model('stock', 'StockItem')
|
||||||
|
|
||||||
|
self.assertEqual(StockItem.objects.count(), 1)
|
||||||
|
item = StockItem.objects.first()
|
||||||
|
self.assertEqual(item.barcode_hash, '12345')
|
||||||
|
self.assertEqual(item.uid, '12345')
|
||||||
|
|
||||||
|
|
||||||
|
class TestBarcodeToUiReversedMigration(MigratorTestCase):
|
||||||
|
"""Unit tests for barcode to uid data migrations."""
|
||||||
|
|
||||||
|
migrate_to = ('stock', '0084_auto_20220903_0154')
|
||||||
|
migrate_from = ('stock', '0085_auto_20220903_0225')
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
"""Create initial data."""
|
||||||
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
|
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
|
||||||
|
|
||||||
|
# Create a test StockItem
|
||||||
|
part = Part.objects.create(name='test', level=0, lft=0, rght=0, tree_id=0)
|
||||||
|
StockItem.objects.create(
|
||||||
|
part_id=part.id, barcode_hash='54321', level=0, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
self.assertEqual(StockItem.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_migration(self):
|
||||||
|
"""Test that the migrations were applied as expected."""
|
||||||
|
StockItem = self.new_state.apps.get_model('stock', 'StockItem')
|
||||||
|
|
||||||
|
self.assertEqual(StockItem.objects.count(), 1)
|
||||||
|
item = StockItem.objects.first()
|
||||||
|
self.assertEqual(item.barcode_hash, '54321')
|
||||||
|
self.assertEqual(item.uid, '54321')
|
||||||
|
|
||||||
|
|
||||||
|
class TestPartTestTemplateTreeFixMigration(MigratorTestCase):
|
||||||
|
"""Unit tests for fixing issues with PartTestTemplate tree branch confusion migrations."""
|
||||||
|
|
||||||
|
migrate_from = ('stock', '0107_remove_stockitemtestresult_test_and_more')
|
||||||
|
migrate_to = ('stock', '0108_auto_20240219_0252')
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
"""Create initial data."""
|
||||||
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
|
PartTestTemplate = self.old_state.apps.get_model('part', 'PartTestTemplate')
|
||||||
|
StockItem = self.old_state.apps.get_model('stock', 'StockItem')
|
||||||
|
StockItemTestResult = self.old_state.apps.get_model(
|
||||||
|
'stock', 'StockItemTestResult'
|
||||||
|
)
|
||||||
|
|
||||||
|
p = Part.objects.create(name='test', level=0, lft=0, rght=0, tree_id=0)
|
||||||
|
p2 = Part.objects.create(name='test 2', level=0, lft=0, rght=0, tree_id=4)
|
||||||
|
tmpl = PartTestTemplate.objects.create(part=p2, key='test_key')
|
||||||
|
stock = StockItem.objects.create(part=p, level=0, lft=3, rght=0, tree_id=0)
|
||||||
|
StockItemTestResult.objects.create(template=tmpl, stock_item=stock)
|
||||||
|
self.assertEqual(StockItemTestResult.objects.count(), 1)
|
||||||
|
self.assertEqual(PartTestTemplate.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_migration(self):
|
||||||
|
"""Test that the migrations were applied as expected."""
|
||||||
|
PartTestTemplate = self.old_state.apps.get_model('part', 'PartTestTemplate')
|
||||||
|
StockItemTestResult = self.old_state.apps.get_model(
|
||||||
|
'stock', 'StockItemTestResult'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(StockItemTestResult.objects.count(), 1)
|
||||||
|
self.assertEqual(PartTestTemplate.objects.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockItemTrackingMigration(MigratorTestCase):
|
||||||
|
"""Unit tests for StockItemTracking code migrations."""
|
||||||
|
|
||||||
|
migrate_from = ('stock', '0095_stocklocation_external')
|
||||||
|
migrate_to = ('stock', '0096_auto_20230330_1121')
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
"""Create initial data."""
|
||||||
|
from stock.status_codes import StockHistoryCode
|
||||||
|
|
||||||
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
|
SalesOrder = self.old_state.apps.get_model('order', 'salesorder')
|
||||||
|
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
|
||||||
|
StockItemTracking = self.old_state.apps.get_model('stock', 'stockitemtracking')
|
||||||
|
|
||||||
|
# Create a test StockItem
|
||||||
|
part = Part.objects.create(name='test', level=0, lft=0, rght=0, tree_id=0)
|
||||||
|
so = SalesOrder.objects.create(reference='123')
|
||||||
|
si = StockItem.objects.create(
|
||||||
|
part_id=part.id, sales_order=so, level=0, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
si2 = StockItem.objects.create(
|
||||||
|
part_id=part.id, sales_order=so, level=0, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
StockItem.objects.create(
|
||||||
|
part_id=part.id, sales_order=so, level=0, lft=0, rght=0, tree_id=0
|
||||||
|
)
|
||||||
|
|
||||||
|
StockItemTracking.objects.create(
|
||||||
|
item_id=si.pk,
|
||||||
|
tracking_type=StockHistoryCode.SENT_TO_CUSTOMER.value,
|
||||||
|
deltas={'foo': 'bar'},
|
||||||
|
)
|
||||||
|
StockItemTracking.objects.create(
|
||||||
|
item_id=si2.pk,
|
||||||
|
tracking_type=StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER.value,
|
||||||
|
deltas={'foo': 'bar'},
|
||||||
|
)
|
||||||
|
self.assertEqual(StockItemTracking.objects.count(), 2)
|
||||||
|
|
||||||
|
def test_migration(self):
|
||||||
|
"""Test that the migrations were applied as expected."""
|
||||||
|
from stock.status_codes import StockHistoryCode
|
||||||
|
|
||||||
|
StockItemTracking = self.old_state.apps.get_model('stock', 'stockitemtracking')
|
||||||
|
|
||||||
|
self.assertEqual(StockItemTracking.objects.count(), 2)
|
||||||
|
item = StockItemTracking.objects.first()
|
||||||
|
self.assertEqual(
|
||||||
|
item.tracking_type, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER
|
||||||
|
)
|
||||||
|
self.assertIn('salesorder', item.deltas)
|
||||||
|
self.assertEqual(item.deltas['salesorder'], 1)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user