From 214f472ec90866d068e6c8678eb56fbd5c42f4ab Mon Sep 17 00:00:00 2001 From: Jayasree Date: Sun, 9 Aug 2026 05:26:48 +0530 Subject: [PATCH] Fix test result ordering by test timestamps (#12533) * Fix test result ordering by test timestamps * Refractor test result comparison helper * Apply formatting fixes * Fix frontend formatting * Avoid mutating test result records * Add migration for test result ordering * Retry documentation build --------- Co-authored-by: jayasree723 Co-authored-by: Oliver --- .../0127_alter_stockitemtestresult_options.py | 25 ++++++++++++ src/backend/InvenTree/stock/models.py | 2 + src/frontend/src/functions/comparison.tsx | 39 +++++++++++++++++++ .../src/tables/build/BuildOutputTable.tsx | 5 +-- .../tables/stock/StockItemTestResultTable.tsx | 35 ++++++++--------- 5 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 src/backend/InvenTree/stock/migrations/0127_alter_stockitemtestresult_options.py diff --git a/src/backend/InvenTree/stock/migrations/0127_alter_stockitemtestresult_options.py b/src/backend/InvenTree/stock/migrations/0127_alter_stockitemtestresult_options.py new file mode 100644 index 0000000000..6246926911 --- /dev/null +++ b/src/backend/InvenTree/stock/migrations/0127_alter_stockitemtestresult_options.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("stock", "0126_serial_number_concurrency_guard"), + ] + + operations = [ + migrations.AlterModelOptions( + name="stockitemtestresult", + options={ + "ordering": ( + "-finished_datetime", + "-started_datetime", + "-date", + "-pk", + ), + "verbose_name": "Stock Item Test Result", + }, + ), + ] diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 1d2fc55748..47182b0687 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -3820,6 +3820,8 @@ class StockItemTestResult(InvenTree.models.InvenTreeMetadataModel): verbose_name = _('Stock Item Test Result') + ordering = ('-finished_datetime', '-started_datetime', '-date', '-pk') + def __str__(self): """Return string representation.""" return f'{self.test_name} - {self.result}' diff --git a/src/frontend/src/functions/comparison.tsx b/src/frontend/src/functions/comparison.tsx index a1f6293f42..e1639f3759 100644 --- a/src/frontend/src/functions/comparison.tsx +++ b/src/frontend/src/functions/comparison.tsx @@ -53,3 +53,42 @@ export function isEquivalent(a: any, b: any): boolean { return false; } + +export function compareTestResults(a: any, b: any) { + const finishedA = a.finished_datetime + ? new Date(a.finished_datetime).getTime() + : null; + const finishedB = b.finished_datetime + ? new Date(b.finished_datetime).getTime() + : null; + + if (finishedA !== finishedB) { + if (finishedA === null) return 1; + if (finishedB === null) return -1; + return finishedB - finishedA; + } + + const startedA = a.started_datetime + ? new Date(a.started_datetime).getTime() + : null; + const startedB = b.started_datetime + ? new Date(b.started_datetime).getTime() + : null; + + if (startedA !== startedB) { + if (startedA === null) return 1; + if (startedB === null) return -1; + return startedB - startedA; + } + + const uploadTimeA = a.date ? new Date(a.date).getTime() : null; + const uploadTimeB = b.date ? new Date(b.date).getTime() : null; + + if (uploadTimeA !== uploadTimeB) { + if (uploadTimeA === null) return 1; + if (uploadTimeB === null) return -1; + return uploadTimeB - uploadTimeA; + } + + return b.pk - a.pk; +} diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index c482971903..aeeabc30b0 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -63,6 +63,7 @@ import { useStockFields, useStockItemSerializeFields } from '../../forms/StockForms'; +import { compareTestResults } from '../../functions/comparison'; import { InvenTreeIcon } from '../../functions/icons'; import useBackgroundTask from '../../hooks/UseBackgroundTask'; import { @@ -277,9 +278,7 @@ export default function BuildOutputTable({ // Find the "newest" result for this template in the returned data const result = record.tests ?.filter((test: any) => test.template == template.pk) - .sort((a: any, b: any) => { - return a.pk < b.pk ? 1 : -1; - }) + .sort(compareTestResults) .shift(); if (template?.required && result?.result) { diff --git a/src/frontend/src/tables/stock/StockItemTestResultTable.tsx b/src/frontend/src/tables/stock/StockItemTestResultTable.tsx index 4cbe2412c9..39119e9ad2 100644 --- a/src/frontend/src/tables/stock/StockItemTestResultTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTestResultTable.tsx @@ -37,6 +37,7 @@ import RowExpansionIcon from '../../components/tables/RowExpansionIcon'; import { useApi } from '../../contexts/ApiContext'; import { formatDate } from '../../defaults/formatters'; import { useTestResultFields } from '../../forms/StockForms'; +import { compareTestResults } from '../../functions/comparison'; import { useCreateApiFormModal, useDeleteApiFormModal, @@ -110,26 +111,22 @@ export default function StockItemTestResultTable({ }); // Iterate through the returned records - // Note that the results are sorted by oldest first, - // to ensure that the most recent result is displayed "on top" - records - .sort((a: any, b: any) => { - return a.pk > b.pk ? 1 : -1; - }) - .forEach((record) => { - // Find matching template - const idx = results.findIndex( - (r: any) => r.templateId == record.template - ); - if (idx >= 0) { - results[idx] = { - ...results[idx], - ...record - }; + // Sort test results using the same priority as the backend: + // finished_datetime -> started_datetime -> date -> pk + records.toSorted(compareTestResults).forEach((record) => { + // Find matching template + const idx = results.findIndex( + (r: any) => r.templateId == record.template + ); + if (idx >= 0) { + results[idx] = { + ...results[idx], + ...record + }; - results[idx].results.push(record); - } - }); + results[idx].results.push(record); + } + }); return results; },