diff --git a/src/backend/InvenTree/common/tests.py b/src/backend/InvenTree/common/tests.py index 6990fc9c18..cdf8ae4a59 100644 --- a/src/backend/InvenTree/common/tests.py +++ b/src/backend/InvenTree/common/tests.py @@ -23,7 +23,12 @@ import PIL import common.validators from common.settings import get_global_setting, set_global_setting 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 plugin import registry from plugin.models import NotificationUserSetting @@ -1676,3 +1681,14 @@ class CustomStatusTest(TestCase): self.assertEqual( 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}, + ) diff --git a/src/backend/InvenTree/plugin/mixins/__init__.py b/src/backend/InvenTree/plugin/mixins/__init__.py index bd5851f667..3b0ea7557f 100644 --- a/src/backend/InvenTree/plugin/mixins/__init__.py +++ b/src/backend/InvenTree/plugin/mixins/__init__.py @@ -39,7 +39,6 @@ __all__ = [ 'SingleNotificationMethod', 'SupplierBarcodeMixin', 'UrlsMixin', - 'UrlsMixin', 'UserInterfaceMixin', 'ValidationMixin', ] diff --git a/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py b/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py index 6acff5cac9..010692cb8b 100644 --- a/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py @@ -1,16 +1,18 @@ """Unit tests for the SampleValidatorPlugin class.""" from django.core.exceptions import ValidationError +from django.urls import reverse +import build.models import part.models -from InvenTree.unit_test import InvenTreeTestCase +from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase from plugin.registry import registry -class SampleValidatorPluginTest(InvenTreeTestCase): +class SampleValidatorPluginTest(InvenTreeAPITestCase, InvenTreeTestCase): """Tests for the SampleValidatonPlugin class.""" - fixtures = ['category', 'location'] + fixtures = ['part', 'category', 'location', 'build'] def setUp(self): """Set up the test environment.""" @@ -28,6 +30,7 @@ class SampleValidatorPluginTest(InvenTreeTestCase): self.bom_item = part.models.BomItem.objects.create( part=self.assembly, sub_part=self.part, quantity=1 ) + super().setUp() def get_plugin(self): """Return the SampleValidatorPlugin instance.""" @@ -113,3 +116,40 @@ class SampleValidatorPluginTest(InvenTreeTestCase): self.part.IPN = 'LMNOPQ' 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' + ) + ) diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index a4036f0551..ce3ea556fc 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -18,6 +18,7 @@ from build.models import Build from common.models import Attachment, InvenTreeSetting from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase from order.models import ReturnOrder, SalesOrder +from part.models import Part from plugin.registry import registry from report.models import LabelTemplate, ReportTemplate from report.templatetags import barcode as barcode_tags @@ -305,6 +306,16 @@ class ReportTest(InvenTreeAPITestCase): response = self.get(url, {'enabled': False}) 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): """Test that creating a new report works for each report.""" url = reverse('api-report-template-list') @@ -533,6 +544,22 @@ class PrintTestMixins: 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): """Unit testing class for the stock item TestReport model.""" diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index d2226f529e..d6fff9e6c4 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -144,7 +144,7 @@ class StockDetail(RetrieveUpdateDestroyAPI): params.get('supplier_part_detail', True) ) kwargs['path_detail'] = str2bool(params.get('path_detail', False)) - except AttributeError: + except AttributeError: # pragma: no cover pass return self.serializer_class(*args, **kwargs) @@ -164,7 +164,7 @@ class StockItemContextMixin: try: context['item'] = StockItem.objects.get(pk=self.kwargs.get('pk', None)) - except Exception: + except Exception: # pragma: no cover pass return context @@ -526,7 +526,8 @@ class StockFilter(rest_filters.FilterSet): def filter_manufacturer(self, queryset, name, company): """Filter by manufacturer.""" 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( @@ -891,7 +892,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView): 'tests', ]: kwargs[key] = str2bool(params.get(key, False)) - except AttributeError: + except AttributeError: # pragma: no cover pass kwargs['context'] = self.get_serializer_context() @@ -982,7 +983,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView): data['purchase_price'] = float( data['purchase_price'] ) / float(supplier_part.pack_quantity_native) - except ValueError: + except ValueError: # pragma: no cover pass # 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)] ) - except (ValueError, StockItem.DoesNotExist): + except (ValueError, StockItem.DoesNotExist): # pragma: no cover pass # 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 queryset = queryset.exclude(pk__in=[a.item.pk for a in allocations]) - except (ValueError, SalesOrder.DoesNotExist): + except (ValueError, SalesOrder.DoesNotExist): # pragma: no cover pass # Does the client wish to filter by the Part ID? @@ -1160,7 +1161,7 @@ class StockList(DataExportViewMixin, ListCreateDestroyAPIView): else: queryset = queryset.filter(location=loc_id) - except (ValueError, StockLocation.DoesNotExist): + except (ValueError, StockLocation.DoesNotExist): # pragma: no cover pass return queryset @@ -1223,7 +1224,7 @@ class StockItemTestResultMixin: kwargs['template_detail'] = str2bool( self.request.query_params.get('template_detail', False) ) - except Exception: + except Exception: # pragma: no cover pass kwargs['context'] = self.get_serializer_context() @@ -1363,7 +1364,7 @@ class StockItemTestResultList(StockItemTestResultMixin, ListCreateDestroyAPIView queryset = queryset.filter(stock_item__in=items) - except (ValueError, StockItem.DoesNotExist): + except (ValueError, StockItem.DoesNotExist): # pragma: no cover pass return queryset @@ -1405,14 +1406,14 @@ class StockTrackingList(DataExportViewMixin, ListAPI): kwargs['item_detail'] = str2bool( self.request.query_params.get('item_detail', False) ) - except Exception: + except Exception: # pragma: no cover pass try: kwargs['user_detail'] = str2bool( self.request.query_params.get('user_detail', False) ) - except Exception: + except Exception: # pragma: no cover pass kwargs['context'] = self.get_serializer_context() diff --git a/src/backend/InvenTree/stock/fixtures/location.yaml b/src/backend/InvenTree/stock/fixtures/location.yaml index 0cbcead07e..9269fe91b4 100644 --- a/src/backend/InvenTree/stock/fixtures/location.yaml +++ b/src/backend/InvenTree/stock/fixtures/location.yaml @@ -41,6 +41,7 @@ tree_id: 2 lft: 1 rght: 8 + external: True - model: stock.stocklocation pk: 5 diff --git a/src/backend/InvenTree/stock/migrations/0065_auto_20210701_0509.py b/src/backend/InvenTree/stock/migrations/0065_auto_20210701_0509.py index 5aedb7c6da..f8718ac885 100644 --- a/src/backend/InvenTree/stock/migrations/0065_auto_20210701_0509.py +++ b/src/backend/InvenTree/stock/migrations/0065_auto_20210701_0509.py @@ -4,22 +4,6 @@ import InvenTree.fields from django.db import migrations 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): @@ -30,10 +14,6 @@ class Migration(migrations.Migration): operations = [] xoperations = [ - migrations.RunPython( - code=show_migrations, - reverse_code=migrations.RunPython.noop - ), migrations.AlterField( model_name='stockitem', name='purchase_price', diff --git a/src/backend/InvenTree/stock/migrations/0108_auto_20240219_0252.py b/src/backend/InvenTree/stock/migrations/0108_auto_20240219_0252.py index 60217577c0..185eeb0f2a 100644 --- a/src/backend/InvenTree/stock/migrations/0108_auto_20240219_0252.py +++ b/src/backend/InvenTree/stock/migrations/0108_auto_20240219_0252.py @@ -43,7 +43,7 @@ def update_templates(apps, schema_editor): # 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 - # Annotate the queryset with a "mathching template" + # Annotate the queryset with a "matching template" template_query = PartTestTemplate.objects.filter( part__tree_id=OuterRef('stock_item__part__tree_id'), diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index f805084033..92660abc2c 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -19,6 +19,7 @@ import build.models import company.models import part.models from common.models import InvenTreeCustomUserStateModel, InvenTreeSetting +from common.settings import set_global_setting from InvenTree.unit_test import InvenTreeAPITestCase from part.models import Part, PartTestTemplate from stock.models import ( @@ -437,6 +438,12 @@ class StockLocationTest(StockAPITestCase): 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): """Test the StockLocationTree API endpoint.""" # Create a number of new locations @@ -586,6 +593,11 @@ class StockItemListTest(StockAPITestCase): 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): """Filter StockItem by Part reference.""" response = self.get_stock(part=25) @@ -745,6 +757,79 @@ class StockItemListTest(StockAPITestCase): response = self.get_stock(expired=0) 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): """Test that we can paginate results correctly.""" 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 ) + 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): """Test that the 'child_items' annotation works as expected.""" # Create a trackable part @@ -1298,6 +1426,18 @@ class StockItemTest(StockAPITestCase): self.assertEqual(trackable_part.stock_entries().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): """Test that the "default_expiry" functionality works via the API. @@ -2259,3 +2399,38 @@ class StockMetadataAPITest(InvenTreeAPITestCase): 'api-stock-item-metadata': StockItem, }.items(): 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, [{}]) diff --git a/src/backend/InvenTree/stock/test_migrations.py b/src/backend/InvenTree/stock/test_migrations.py index 11810869f4..6407f5ea29 100644 --- a/src/backend/InvenTree/stock/test_migrations.py +++ b/src/backend/InvenTree/stock/test_migrations.py @@ -231,3 +231,189 @@ class TestTestResultMigration(MigratorTestCase): for result in StockItemTestResult.objects.all(): 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)