mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Added test statistics (#7164)
* Added test statistics Fixed #5995 * Bump API version * Fix javascript varible scopes * Fix javascript exports * Remove duplicated import * Add files modified by the pre-commit scripts * Move test statistics API urls to a separate endpoint * Merge test-statistics urls * Undo unrelated changes * Formatting fix * Fix API urls for test statistics in PUI * Fix prefixing in test statistic functions --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
		| @@ -1,12 +1,15 @@ | ||||
| """InvenTree API version information.""" | ||||
|  | ||||
| # InvenTree API version | ||||
| INVENTREE_API_VERSION = 229 | ||||
| INVENTREE_API_VERSION = 230 | ||||
|  | ||||
| """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | ||||
|  | ||||
|  | ||||
| INVENTREE_API_TEXT = """ | ||||
| v230 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164 | ||||
|     - Adds test statistics endpoint | ||||
|  | ||||
| v229 - 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775 | ||||
|     - Add extra exportable fields to the BomItem serializer | ||||
|  | ||||
|   | ||||
| @@ -1112,3 +1112,8 @@ a { | ||||
|     -webkit-font-smoothing: antialiased; | ||||
|     -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
|  | ||||
| .test-statistics-table-total-row { | ||||
|     font-weight: bold; | ||||
|     border-top-style: double; | ||||
| } | ||||
|   | ||||
| @@ -35,6 +35,7 @@ from company.urls import company_urls, manufacturer_part_urls, supplier_part_url | ||||
| from order.urls import order_urls | ||||
| from part.urls import part_urls | ||||
| from plugin.urls import get_plugin_urls | ||||
| from stock.api import test_statistics_api_urls | ||||
| from stock.urls import stock_urls | ||||
| from web.urls import api_urls as web_api_urls | ||||
| from web.urls import urlpatterns as platform_urls | ||||
| @@ -109,6 +110,7 @@ apipatterns = [ | ||||
|             ), | ||||
|         ]), | ||||
|     ), | ||||
|     path('test-statistics/', include(test_statistics_api_urls)), | ||||
|     path('user/', include(users.api.user_urls)), | ||||
|     path('web/', include(web_api_urls)), | ||||
|     # Plugin endpoints | ||||
|   | ||||
| @@ -298,6 +298,12 @@ src="{% static 'img/blank_image.png' %}" | ||||
|             build: {{ build.pk }}, | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     {% if build.part.trackable > 0 %} | ||||
|     onPanelLoad("test-statistics", function() { | ||||
|         prepareTestStatisticsTable('build', '{% url "api-test-statistics-by-build" build.pk %}') | ||||
|     }); | ||||
|     {% endif %} | ||||
|     {% endif %} | ||||
|     {% endif %} | ||||
|  | ||||
|   | ||||
| @@ -267,6 +267,21 @@ | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class='panel panel-hidden' id='panel-test-statistics'> | ||||
|     <div class='panel-heading'> | ||||
|         <h4> | ||||
|             {% trans "Build test statistics" %} | ||||
|         </h4> | ||||
|     </div> | ||||
|  | ||||
|     <div class='panel-content'> | ||||
|         <div id='teststatistics-button-toolbar'> | ||||
|             {% include "filter_list.html" with id="buildteststatistics" %} | ||||
|         </div> | ||||
|         {% include "test_statistics_table.html" with prefix="build-" %} | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class='panel panel-hidden' id='panel-attachments'> | ||||
|     <div class='panel-heading'> | ||||
|         <div class='d-flex flex-wrap'> | ||||
|   | ||||
| @@ -20,6 +20,10 @@ | ||||
| {% include "sidebar_item.html" with label='consumed' text=text icon="fa-tasks" %} | ||||
| {% trans "Child Build Orders" as text %} | ||||
| {% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %} | ||||
| {% if build.part.trackable %} | ||||
| {% trans "Test Statistics" as text %} | ||||
| {% include "sidebar_item.html" with label='test-statistics' text=text icon="fa-chart-line" %} | ||||
| {% endif %} | ||||
| {% trans "Attachments" as text %} | ||||
| {% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %} | ||||
| {% trans "Notes" as text %} | ||||
|   | ||||
| @@ -100,6 +100,22 @@ | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class='panel panel-hidden' id='panel-test-statistics'> | ||||
|     <div class='panel-heading'> | ||||
|         <div class='d-flex flex-wrap'> | ||||
|             <h4>{% trans "Part Test Statistics" %}</h4> | ||||
|             {% include "spacer.html" %} | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class='panel-content'> | ||||
|         <div id='teststatistics-button-toolbar'> | ||||
|             {% include "filter_list.html" with id="partteststatistics" %} | ||||
|         </div> | ||||
|  | ||||
|         {% include "test_statistics_table.html" with prefix="part-" %} | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class='panel panel-hidden' id='panel-purchase-orders'> | ||||
|     <div class='panel-heading'> | ||||
|         <div class='d-flex flex-wrap'> | ||||
| @@ -751,6 +767,9 @@ | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|     onPanelLoad("test-statistics", function() { | ||||
|         prepareTestStatisticsTable('part', '{% url "api-test-statistics-by-part" part.pk %}') | ||||
|     }); | ||||
|  | ||||
|     onPanelLoad("part-stock", function() { | ||||
|         $('#new-stock-item').click(function () { | ||||
|   | ||||
| @@ -53,6 +53,8 @@ | ||||
| {% if part.trackable %} | ||||
| {% trans "Test Templates" as text %} | ||||
| {% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %} | ||||
| {% trans "Test Statistics" as text %} | ||||
| {% include "sidebar_item.html" with label="test-statistics" text=text icon="fa-chart-line" %} | ||||
| {% endif %} | ||||
| {% if show_related %} | ||||
| {% trans "Related Parts" as text %} | ||||
|   | ||||
| @@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from django_filters import rest_framework as rest_filters | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import extend_schema_field | ||||
| from drf_spectacular.utils import extend_schema, extend_schema_field | ||||
| from rest_framework import permissions, status | ||||
| from rest_framework.generics import GenericAPIView | ||||
| from rest_framework.response import Response | ||||
| @@ -1288,6 +1288,54 @@ class StockItemTestResultFilter(rest_filters.FilterSet): | ||||
|         return queryset.filter(template__key=key) | ||||
|  | ||||
|  | ||||
| class TestStatisticsFilter(rest_filters.FilterSet): | ||||
|     """API filter for the filtering the test results belonging to a specific build.""" | ||||
|  | ||||
|     class Meta: | ||||
|         """Metaclass options.""" | ||||
|  | ||||
|         model = StockItemTestResult | ||||
|         fields = [] | ||||
|  | ||||
|     # Created date filters | ||||
|     finished_before = InvenTreeDateFilter( | ||||
|         label='Finished before', field_name='finished_datetime', lookup_expr='lte' | ||||
|     ) | ||||
|     finished_after = InvenTreeDateFilter( | ||||
|         label='Finished after', field_name='finished_datetime', lookup_expr='gte' | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class TestStatistics(GenericAPIView): | ||||
|     """API endpoint for accessing a test statistics broken down by test templates.""" | ||||
|  | ||||
|     queryset = StockItemTestResult.objects.all() | ||||
|     serializer_class = StockSerializers.TestStatisticsSerializer | ||||
|     pagination_class = None | ||||
|     filterset_class = TestStatisticsFilter | ||||
|     filter_backends = SEARCH_ORDER_FILTER_ALIAS | ||||
|  | ||||
|     @extend_schema( | ||||
|         responses={200: StockSerializers.TestStatisticsSerializer(many=False)} | ||||
|     ) | ||||
|     def get(self, request, pk, *args, **kwargs): | ||||
|         """Return test execution count matrix broken down by test result.""" | ||||
|         instance = self.get_object() | ||||
|         serializer = self.get_serializer(instance) | ||||
|         if request.resolver_match.url_name == 'api-test-statistics-by-part': | ||||
|             serializer.context['type'] = 'by-part' | ||||
|         elif request.resolver_match.url_name == 'api-test-statistics-by-build': | ||||
|             serializer.context['type'] = 'by-build' | ||||
|         serializer.context['finished_datetime_after'] = self.request.query_params.get( | ||||
|             'finished_datetime_after' | ||||
|         ) | ||||
|         serializer.context['finished_datetime_before'] = self.request.query_params.get( | ||||
|             'finished_datetime_before' | ||||
|         ) | ||||
|         serializer.context['pk'] = pk | ||||
|         return Response([serializer.data]) | ||||
|  | ||||
|  | ||||
| class StockItemTestResultList(StockItemTestResultMixin, ListCreateDestroyAPIView): | ||||
|     """API endpoint for listing (and creating) a StockItemTestResult object.""" | ||||
|  | ||||
| @@ -1663,3 +1711,27 @@ stock_api_urls = [ | ||||
|     # Anything else | ||||
|     path('', StockList.as_view(), name='api-stock-list'), | ||||
| ] | ||||
|  | ||||
| test_statistics_api_urls = [ | ||||
|     # Test statistics endpoints | ||||
|     path( | ||||
|         'by-part/', | ||||
|         include([ | ||||
|             path( | ||||
|                 '<int:pk>/', | ||||
|                 TestStatistics.as_view(), | ||||
|                 name='api-test-statistics-by-part', | ||||
|             ) | ||||
|         ]), | ||||
|     ), | ||||
|     path( | ||||
|         'by-build/', | ||||
|         include([ | ||||
|             path( | ||||
|                 '<int:pk>/', | ||||
|                 TestStatistics.as_view(), | ||||
|                 name='api-test-statistics-by-build', | ||||
|             ) | ||||
|         ]), | ||||
|     ), | ||||
| ] | ||||
|   | ||||
| @@ -33,6 +33,7 @@ import InvenTree.ready | ||||
| import InvenTree.tasks | ||||
| import report.mixins | ||||
| import report.models | ||||
| from build import models as BuildModels | ||||
| from common.icons import validate_icon | ||||
| from common.settings import get_global_setting | ||||
| from company import models as CompanyModels | ||||
| @@ -40,6 +41,7 @@ from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField | ||||
| from order.status_codes import SalesOrderStatusGroups | ||||
| from part import models as PartModels | ||||
| from plugin.events import trigger_event | ||||
| from stock import models as StockModels | ||||
| from stock.generators import generate_batch_code | ||||
| from stock.status_codes import StockHistoryCode, StockStatus, StockStatusGroups | ||||
| from users.models import Owner | ||||
| @@ -2459,6 +2461,67 @@ class StockItemTestResult(InvenTree.models.InvenTreeMetadataModel): | ||||
|         """Return key for test.""" | ||||
|         return InvenTree.helpers.generateTestKey(self.test_name) | ||||
|  | ||||
|     def calculate_test_statistics_for_test_template( | ||||
|         self, query_base, test_template, ret, start, end | ||||
|     ): | ||||
|         """Helper function to calculate the passed/failed/total tests count per test template type.""" | ||||
|         query = query_base & Q(template=test_template.pk) | ||||
|         if start is not None and end is not None: | ||||
|             query = query & Q(started_datetime__range=(start, end)) | ||||
|         elif start is not None and end is None: | ||||
|             query = query & Q(started_datetime__gt=start) | ||||
|         elif start is None and end is not None: | ||||
|             query = query & Q(started_datetime__lt=end) | ||||
|  | ||||
|         passed = StockModels.StockItemTestResult.objects.filter( | ||||
|             query & Q(result=True) | ||||
|         ).count() | ||||
|         failed = StockModels.StockItemTestResult.objects.filter( | ||||
|             query & ~Q(result=True) | ||||
|         ).count() | ||||
|         if test_template.test_name not in ret: | ||||
|             ret[test_template.test_name] = {'passed': 0, 'failed': 0, 'total': 0} | ||||
|         ret[test_template.test_name]['passed'] += passed | ||||
|         ret[test_template.test_name]['failed'] += failed | ||||
|         ret[test_template.test_name]['total'] += passed + failed | ||||
|         ret['total']['passed'] += passed | ||||
|         ret['total']['failed'] += failed | ||||
|         ret['total']['total'] += passed + failed | ||||
|         return ret | ||||
|  | ||||
|     def build_test_statistics(self, build_order_pk, start, end): | ||||
|         """Generate a statistics matrix for each test template based on the test executions result counts.""" | ||||
|         build = BuildModels.Build.objects.get(pk=build_order_pk) | ||||
|         if not build or not build.part.trackable: | ||||
|             return {} | ||||
|  | ||||
|         test_templates = build.part.getTestTemplates() | ||||
|         ret = {'total': {'passed': 0, 'failed': 0, 'total': 0}} | ||||
|         for build_item in build.get_build_outputs(): | ||||
|             for test_template in test_templates: | ||||
|                 query_base = Q(stock_item=build_item) | ||||
|                 ret = self.calculate_test_statistics_for_test_template( | ||||
|                     query_base, test_template, ret, start, end | ||||
|                 ) | ||||
|         return ret | ||||
|  | ||||
|     def part_test_statistics(self, part_pk, start, end): | ||||
|         """Generate a statistics matrix for each test template based on the test executions result counts.""" | ||||
|         part = PartModels.Part.objects.get(pk=part_pk) | ||||
|  | ||||
|         if not part or not part.trackable: | ||||
|             return {} | ||||
|  | ||||
|         test_templates = part.getTestTemplates() | ||||
|         ret = {'total': {'passed': 0, 'failed': 0, 'total': 0}} | ||||
|         for bo in part.stock_entries(): | ||||
|             for test_template in test_templates: | ||||
|                 query_base = Q(stock_item=bo) | ||||
|                 ret = self.calculate_test_statistics_for_test_template( | ||||
|                     query_base, test_template, ret, start, end | ||||
|                 ) | ||||
|         return ret | ||||
|  | ||||
|     stock_item = models.ForeignKey( | ||||
|         StockItem, on_delete=models.CASCADE, related_name='test_results' | ||||
|     ) | ||||
|   | ||||
| @@ -870,6 +870,36 @@ class UninstallStockItemSerializer(serializers.Serializer): | ||||
|         item.uninstall_into_location(location, request.user, note) | ||||
|  | ||||
|  | ||||
| class TestStatisticsLineField(serializers.DictField): | ||||
|     """DRF field definition for one column of the test statistics.""" | ||||
|  | ||||
|     test_name = serializers.CharField() | ||||
|     results = serializers.DictField(child=serializers.IntegerField(min_value=0)) | ||||
|  | ||||
|  | ||||
| class TestStatisticsSerializer(serializers.Serializer): | ||||
|     """DRF serializer class for the test statistics.""" | ||||
|  | ||||
|     results = serializers.ListField(child=TestStatisticsLineField(), read_only=True) | ||||
|  | ||||
|     def to_representation(self, obj): | ||||
|         """Just pass through the test statistics from the model.""" | ||||
|         if self.context['type'] == 'by-part': | ||||
|             return obj.part_test_statistics( | ||||
|                 self.context['pk'], | ||||
|                 self.context['finished_datetime_after'], | ||||
|                 self.context['finished_datetime_before'], | ||||
|             ) | ||||
|         elif self.context['type'] == 'by-build': | ||||
|             return obj.build_test_statistics( | ||||
|                 self.context['pk'], | ||||
|                 self.context['finished_datetime_after'], | ||||
|                 self.context['finished_datetime_before'], | ||||
|             ) | ||||
|  | ||||
|         raise ValidationError(_('Unsupported statistic type: ' + self.context['type'])) | ||||
|  | ||||
|  | ||||
| class ConvertStockItemSerializer(serializers.Serializer): | ||||
|     """DRF serializer class for converting a StockItem to a valid variant part.""" | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,9 @@ | ||||
|  | ||||
| /* exported | ||||
|    setupFilterList, | ||||
|    addTableFilter, | ||||
|    removeTableFilter, | ||||
|    reloadTableFilters | ||||
| */ | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|  | ||||
| /* globals | ||||
|     addCachedAlert, | ||||
|     addTableFilter, | ||||
|     baseCurrency, | ||||
|     calculateTotalPrice, | ||||
|     clearFormInput, | ||||
| @@ -38,8 +39,11 @@ | ||||
|     makeIconBadge, | ||||
|     makeIconButton, | ||||
|     makeRemoveButton, | ||||
|     moment, | ||||
|     orderParts, | ||||
|     partDetail, | ||||
|     reloadTableFilters, | ||||
|     removeTableFilter, | ||||
|     renderClipboard, | ||||
|     renderDate, | ||||
|     renderLink, | ||||
| @@ -70,6 +74,7 @@ | ||||
|     duplicateStockItem, | ||||
|     editStockItem, | ||||
|     editStockLocation, | ||||
|     filterTestStatisticsTableDateRange, | ||||
|     findStockItemBySerialNumber, | ||||
|     installStockItem, | ||||
|     loadInstalledInTable, | ||||
| @@ -78,6 +83,7 @@ | ||||
|     loadStockTestResultsTable, | ||||
|     loadStockTrackingTable, | ||||
|     loadTableFilters, | ||||
|     prepareTestStatisticsTable, | ||||
|     mergeStockItems, | ||||
|     removeStockRow, | ||||
|     serializeStockItem, | ||||
| @@ -3411,3 +3417,105 @@ function setStockStatus(items, options={}) { | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Load TestStatistics table. | ||||
|  */ | ||||
| function loadTestStatisticsTable(table, prefix, url, options, filters = {}) { | ||||
|     inventreeGet(url, filters, { | ||||
|         async: true, | ||||
|         success: function(data) { | ||||
|             const keys = ['passed', 'failed', 'total'] | ||||
|             let header = ''; | ||||
|             let rows = [] | ||||
|             let passed= ''; | ||||
|             let failed = ''; | ||||
|             let total = ''; | ||||
|             $('.test-stat-result-cell').remove(); | ||||
|             $.each(data[0], function(key, value){ | ||||
|                 if (key != "total") { | ||||
|                     header += '<th class="test-stat-result-cell">' + key + '</th>'; | ||||
|                     keys.forEach(function(keyName) { | ||||
|                         var tdText = '-'; | ||||
|                         if (value['total'] != '0' && value[keyName] != '0') { | ||||
|                             let percentage = '' | ||||
|                             if (keyName != 'total' && value[total] != 0) { | ||||
|                                 percentage = ' (' + (100.0 * (parseFloat(value[keyName]) / parseFloat(value['total']))).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 2}) + '%)'; | ||||
|                             } | ||||
|                             tdText = value[keyName] + percentage; | ||||
|                         } | ||||
|                         rows[keyName] += '<td class="test-stat-result-cell">' + tdText + '</td>'; | ||||
|                     }) | ||||
|                 } | ||||
|             }); | ||||
|             $('#' + prefix + '-test-statistics-table-header-id').after(header); | ||||
|  | ||||
|             keys.forEach(function(keyName) { | ||||
|                 let valueStr = data[0]['total'][keyName]; | ||||
|                 if (keyName != 'total' && data[0]['total']['total'] != '0') { | ||||
|                     valueStr += ' (' + (100.0 * (parseFloat(valueStr) / parseFloat(data[0]['total']['total']))).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 2}) + '%)'; | ||||
|                 } | ||||
|                 rows[keyName] += '<td class="test-stat-result-cell">' + valueStr + '</td>'; | ||||
|                 $('#' + prefix + '-test-statistics-table-body-' + keyName).after(rows[keyName]); | ||||
|             }); | ||||
|             $('#' + prefix + '-test-statistics-table').show(); | ||||
|             setupFilterList(prefix + "teststatistics", table, "#filter-list-" + prefix + "teststatistics", options); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function prepareTestStatisticsTable(keyName, apiUrl) | ||||
| { | ||||
|     let options = { | ||||
|         custom_actions: [ | ||||
|             { | ||||
|                 icon: 'fa-calendar-week', | ||||
|                 actions: [ | ||||
|                     { | ||||
|                         icon: 'fa-calendar-week', | ||||
|                         title: '{% trans "This week" %}', | ||||
|                         label: 'this-week', | ||||
|                         callback: function(data) { | ||||
|                             filterTestStatisticsTableDateRange(data, 'this-week', $("#test-statistics-table"), keyName + 'teststatistics', options); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         icon: 'fa-calendar-week', | ||||
|                         title: '{% trans "This month" %}', | ||||
|                         label: 'this-month', | ||||
|                         callback: function(data) { | ||||
|                             filterTestStatisticsTableDateRange(data, 'this-month', $("#test-statistics-table"), keyName + 'teststatistics', options); | ||||
|                         } | ||||
|                     }, | ||||
|                 ], | ||||
|             } | ||||
|         ], | ||||
|         callback: function(table, filters, options) { | ||||
|             loadTestStatisticsTable($("#test-statistics-table"), keyName, apiUrl, options, filters); | ||||
|         } | ||||
|     } | ||||
|     setupFilterList(keyName + 'teststatistics', $("#test-statistics-table"), '#filter-list-' + keyName + 'teststatistics', options); | ||||
|  | ||||
|     // Load test statistics table | ||||
|     loadTestStatisticsTable($("#test-statistics-table"), keyName, apiUrl, options); | ||||
| } | ||||
|  | ||||
| function filterTestStatisticsTableDateRange(data, range, table, tableKey, options) | ||||
| { | ||||
|     var startDateString = ''; | ||||
|     var d = new Date(); | ||||
|     if (range == "this-week") { | ||||
|         startDateString = moment(new Date(d.getFullYear(), d.getMonth(), d.getDate() - (d.getDay() == 0 ? 6 : d.getDay() - 1))).format('YYYY-MM-DD'); | ||||
|     } else if (range == "this-month") { | ||||
|         startDateString = moment(new Date(d.getFullYear(), d.getMonth(), 1)).format('YYYY-MM-DD'); | ||||
|     } else { | ||||
|         console.warn(`Invalid range specified for filterTestStatisticsTableDateRange`); | ||||
|         return; | ||||
|     } | ||||
|     var filters = addTableFilter(tableKey, 'finished_datetime_after', startDateString); | ||||
|     removeTableFilter(tableKey, 'finished_datetime_before') | ||||
|  | ||||
|     reloadTableFilters(table, filters, options); | ||||
|     setupFilterList(tableKey, table, "#filter-list-" + tableKey, options); | ||||
| } | ||||
|   | ||||
| @@ -462,6 +462,20 @@ function getStockTestTableFilters() { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Return a dictionary of filters for the "test statistics" table | ||||
| function getTestStatisticsTableFilters() { | ||||
|  | ||||
|     return { | ||||
|         finished_datetime_after: { | ||||
|             type: 'date', | ||||
|             title: '{% trans "Interval start" %}', | ||||
|         }, | ||||
|         finished_datetime_before: { | ||||
|             type: 'date', | ||||
|             title: '{% trans "Interval end" %}', | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Return a dictionary of filters for the "stocktracking" table | ||||
| function getStockTrackingTableFilters() { | ||||
| @@ -863,6 +877,8 @@ function getAvailableTableFilters(tableKey) { | ||||
|         return getBuildItemTableFilters(); | ||||
|     case 'buildlines': | ||||
|         return getBuildLineTableFilters(); | ||||
|     case 'buildteststatistics': | ||||
|         return getTestStatisticsTableFilters(); | ||||
|     case 'bom': | ||||
|         return getBOMTableFilters(); | ||||
|     case 'category': | ||||
| @@ -885,6 +901,8 @@ function getAvailableTableFilters(tableKey) { | ||||
|         return getPartTableFilters(); | ||||
|     case 'parttests': | ||||
|         return getPartTestTemplateFilters(); | ||||
|     case 'partteststatistics': | ||||
|         return getTestStatisticsTableFilters(); | ||||
|     case 'plugins': | ||||
|         return getPluginTableFilters(); | ||||
|     case 'purchaseorder': | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/backend/InvenTree/templates/test_statistics_table.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/backend/InvenTree/templates/test_statistics_table.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| {% load i18n %} | ||||
| {% load inventree_extras %} | ||||
|  | ||||
| <table class='table table-striped table-condensed' id='{{ prefix }}test-statistics-table' style="display: none;"> | ||||
|     <thead> | ||||
|         <tr> | ||||
|             <th id="{{ prefix }}test-statistics-table-header-id"></th> | ||||
|             <th id="{{ prefix }}test-statistics-table-header-total">{% trans "Total" %}</th> | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|         <tr id="{{ prefix }}test-statistics-table-passed-row"> | ||||
|             <td id="{{ prefix }}test-statistics-table-body-passed">{% trans "Passed" %}</td> | ||||
|         </tr> | ||||
|         <tr id="{{ prefix }}test-statistics-table-failed-row"> | ||||
|             <td id="{{ prefix }}test-statistics-table-body-failed">{% trans "Failed" %}</td> | ||||
|         </tr> | ||||
|         <tr id="{{ prefix }}test-statistics-table-total-row" class="test-statistics-table-total-row"> | ||||
|             <td id="{{ prefix }}test-statistics-table-body-total">{% trans "Total" %}</td> | ||||
|         </tr> | ||||
|     </tbody> | ||||
| </table> | ||||
		Reference in New Issue
	
	Block a user