diff --git a/InvenTree/report/migrations/0013_testreport_include_installed.py b/InvenTree/report/migrations/0013_testreport_include_installed.py new file mode 100644 index 0000000000..3a535bf172 --- /dev/null +++ b/InvenTree/report/migrations/0013_testreport_include_installed.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2021-02-19 04:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0012_buildreport'), + ] + + operations = [ + migrations.AddField( + model_name='testreport', + name='include_installed', + field=models.BooleanField(default=False, help_text='Include test results for stock items installed inside assembled item', verbose_name='Include Installed Tests'), + ), + ] diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 4ab6a25bf4..00777449fc 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -281,6 +281,12 @@ class TestReport(ReportTemplateBase): ] ) + include_installed = models.BooleanField( + default=False, + verbose_name=_('Include Installed Tests'), + help_text=_('Include test results for stock items installed inside assembled item') + ) + def matches_stock_item(self, item): """ Test if this report template matches a given StockItem objects @@ -304,8 +310,8 @@ class TestReport(ReportTemplateBase): return { 'stock_item': stock_item, 'part': stock_item.part, - 'results': stock_item.testResultMap(), - 'result_list': stock_item.testResultList() + 'results': stock_item.testResultMap(include_installed=self.include_installed), + 'result_list': stock_item.testResultList(include_installed=self.include_installed) } diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index ef934f73d0..1be988fed0 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -707,6 +707,41 @@ class StockItem(MPTTModel): return True + def get_installed_items(self, cascade=False): + """ + Return all stock items which are *installed* in this one! + + Args: + cascade - Include items which are installed in items which are installed in items + + Note: This function is recursive, and may result in a number of database hits! + """ + + installed = set() + + items = StockItem.objects.filter(belongs_to=self) + + for item in items: + + # Prevent duplication or recursion + if item == self or item in installed: + continue + + installed.add(item) + + if cascade: + sub_items = item.get_installed_items(cascade=True) + + for sub_item in sub_items: + + # Prevent recursion + if sub_item == self or sub_item in installed: + continue + + installed.add(sub_item) + + return installed + def installedItemCount(self): """ Return the number of stock items installed inside this one. @@ -1305,6 +1340,9 @@ class StockItem(MPTTModel): as all named tests are accessible. """ + # Do we wish to include test results from installed items? + include_installed = kwargs.pop('include_installed', False) + # Filter results by "date", so that newer results # will override older ones. results = self.getTestResults(**kwargs).order_by('date') @@ -1315,6 +1353,20 @@ class StockItem(MPTTModel): key = helpers.generateTestKey(result.test) result_map[key] = result + # Do we wish to "cascade" and include test results from installed stock items? + cascade = kwargs.get('cascade', False) + + if include_installed: + installed_items = self.get_installed_items(cascade=cascade) + + for item in installed_items: + item_results = item.testResultMap() + + for key in item_results.keys(): + # Results from sub items should not override master ones + if key not in result_map.keys(): + result_map[key] = item_results[key] + return result_map def testResultList(self, **kwargs): diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index b54411b0d2..f3ca949bbf 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -622,3 +622,62 @@ class TestResultTest(StockTest): item3 = StockItem.objects.get(serial=100, part=item2.part) self.assertEqual(item3.test_results.count(), 4) + + def test_installed_tests(self): + """ + Test test results for stock in stock. + + Or, test "test results" for "stock items" installed "inside" a "stock item" + """ + + # Get a "master" stock item + item = StockItem.objects.get(pk=105) + + tests = item.testResultMap(include_installed=False) + self.assertEqual(len(tests), 3) + + # There are no "sub items" intalled at this stage + tests = item.testResultMap(include_installed=False) + self.assertEqual(len(tests), 3) + + # Create a stock item which is installed *inside* the master item + sub_item = StockItem.objects.create( + part=item.part, + quantity=1, + belongs_to=item, + location=None + ) + + # Now, create some test results against the sub item + + # First test is overshadowed by the same test for the parent part + StockItemTestResult.objects.create( + stock_item=sub_item, + test='firmware version', + date=datetime.datetime.now().date(), + result=True + ) + + # Should return the same number of tests as before + tests = item.testResultMap(include_installed=True) + self.assertEqual(len(tests), 3) + + # Now, add a *unique* test result for the sub item + StockItemTestResult.objects.create( + stock_item=sub_item, + test='some new test', + date=datetime.datetime.now().date(), + result=False, + value='abcde', + ) + + tests = item.testResultMap(include_installed=True) + self.assertEqual(len(tests), 4) + + self.assertIn('somenewtest', tests) + self.assertEqual(sub_item.test_results.count(), 2) + + # Check that asking for test result map for *top item only* still works + tests = item.testResultMap(include_installed=False) + self.assertEqual(len(tests), 3) + self.assertNotIn('somenewtest', tests)