From 2497dd54254e67e501be5a9ce5e8aaa87c6911c4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 9 Nov 2026 14:55:48 +1100 Subject: [PATCH] [bug] date import (#13225) - Handle date values during data import - reported via sentry.io --- src/backend/InvenTree/importer/models.py | 29 ++++++++++++++++++++---- src/backend/InvenTree/importer/tests.py | 24 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py index 796b15ed8c..d8512ac351 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -2,7 +2,7 @@ import json from collections import OrderedDict -from datetime import datetime +from datetime import date, datetime from typing import Optional from django.contrib.auth.models import User @@ -859,23 +859,44 @@ class DataImportRow(models.Model): if commit: self.save() - def convert_date_field(self, value: str) -> Optional[str]: - """Convert an incoming date field to the correct format for the database.""" + def convert_date_field(self, value) -> Optional[str]: + """Convert an incoming date field to the correct format for the database. + + The value may be a string (CSV / JSON import), or a native date / datetime + object (Excel import via tablib/openpyxl returns date-formatted cells as + datetime.datetime objects). + """ if value in [None, '']: return None + # Native date / datetime objects (note: datetime is a subclass of date) + if isinstance(value, date): + return value.strftime('%Y-%m-%d') + + value = str(value).strip() + + if not value: + return None + # Attempt conversion using accepted formats date_formats = ['%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y', '%Y/%m/%d'] for fmt in date_formats: try: - dt = datetime.strptime(value.strip(), fmt) + dt = datetime.strptime(value, fmt) # If the date is valid, convert it to the standard format and return return dt.strftime('%Y-%m-%d') except ValueError: continue + # Full ISO-8601 datetime string - e.g. a native datetime which has been + # round-tripped through DjangoJSONEncoder when row_data was saved + try: + return datetime.fromisoformat(value).strftime('%Y-%m-%d') + except ValueError: + pass + # If none of the formats matched, return the original value return value diff --git a/src/backend/InvenTree/importer/tests.py b/src/backend/InvenTree/importer/tests.py index 13f1e428b3..a36f649ae5 100644 --- a/src/backend/InvenTree/importer/tests.py +++ b/src/backend/InvenTree/importer/tests.py @@ -143,6 +143,30 @@ class ImporterTest(ImporterMixin, InvenTreeTestCase): row = DataImportRow.objects.get(session=session, row_index=0) self.assertEqual(row.row_data['COMMENT'], '2024-05-01T12:30:00') + def test_convert_date_field(self): + """Test conversion of date values for import. + + Regression test for a Sentry crash: "'datetime.datetime' object has no + attribute 'strip'". Excel imports provide date cells as native datetime + objects, which convert_date_field() previously assumed to be strings. + """ + row = DataImportRow(row_index=0, row_data={}) + + for value, expected in [ + (None, None), + ('', None), + (' ', None), + (datetime.datetime(2024, 5, 1, 12, 30), '2024-05-01'), + (datetime.date(2024, 5, 1), '2024-05-01'), + # Native datetime after round-trip through DjangoJSONEncoder + ('2024-05-01T12:30:00', '2024-05-01'), + (' 2024-05-01 ', '2024-05-01'), + ('01/05/2024', '2024-05-01'), + ('2024/05/01', '2024-05-01'), + ('not a date', 'not a date'), + ]: + self.assertEqual(row.convert_date_field(value), expected, msg=value) + def test_import_header_whitespace(self): """Test that column headers with leading/trailing whitespace are handled correctly.