diff --git a/docs/docs/report/helpers.md b/docs/docs/report/helpers.md index 558561c9eb..1aa82e2f29 100644 --- a/docs/docs/report/helpers.md +++ b/docs/docs/report/helpers.md @@ -409,9 +409,22 @@ For rendering date and datetime information, the following helper functions are Both functions resolve their output using the following priority order: -1. **`fmt=` argument** — a [strftime format string](https://docs.python.org/3/library/datetime.html#format-codes). When provided, this takes full priority and the `locale` argument is ignored. -2. **`locale=` argument** (or the default system locale setting) — when no `fmt` is given, Babel formats the value in a locale-aware *medium* style (e.g. `Jan 12, 2025` for `en-us`). -3. **ISO 8601 fallback** — when neither `fmt` nor a locale is available, the value is returned in ISO format (e.g. `2025-01-12`). +1. **`fmt=` argument** — a [strftime format string](https://docs.python.org/3/library/datetime.html#format-codes). When provided, this takes full priority; `locale` and `date_format` are ignored. +2. **`locale=` argument** — when no `fmt` is given, Babel formats the value using the style set by `date_format` (default `medium`). +3. **Server `LANGUAGE_CODE`** — used as the locale when no `locale=` argument is supplied. + +#### Date Format Styles + +The `date_format` argument controls how Babel renders the date when locale-aware formatting is used. The four named styles are: + +| Style | `format_date` example (en-us, 2025-01-12) | `format_datetime` example (en-us, 2025-01-12 14:30) | +| --- | --- | --- | +| `full` | `Sunday, January 12, 2025` | `Sunday, January 12, 2025 at 2:30:00 PM UTC` | +| `long` | `January 12, 2025` | `January 12, 2025 at 2:30:00 PM UTC` | +| `medium` *(default)* | `Jan 12, 2025` | `Jan 12, 2025, 2:30:00 PM` | +| `short` | `1/12/25` | `1/12/25, 2:30 PM` | + +The exact output varies by locale — the table above uses `en-us`. ### format_date @@ -426,23 +439,25 @@ Both functions resolve their output using the following priority order: {% raw %} {% load report %} - + {% format_date my_date %} - + - + {% format_date my_date fmt="%d/%m/%Y" %} - + {% format_date my_date locale='en-us' %} -{% format_date my_date locale='de-de' %} - + +{% format_date my_date locale='en-us' date_format='short' %} + - -{% format_date my_date timezone="Australia/Sydney" locale='en-au' %} + +{% format_date my_date locale='en-us' date_format='full' %} + {% endraw %} ``` @@ -460,20 +475,21 @@ Both functions resolve their output using the following priority order: {% raw %} {% load report %} - + {% format_datetime my_datetime %} - + - + {% format_datetime my_datetime fmt="%d-%m-%Y %H:%M" %} - + {% format_datetime my_datetime locale='en-us' %} -{% format_datetime my_datetime locale='de-de' %} - + +{% format_datetime my_datetime locale='de-de' date_format='short' %} + {% format_datetime my_datetime timezone="Australia/Sydney" locale='en-au' %} diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index bf49e55c0b..8561deb2e5 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -982,6 +982,7 @@ def format_number( max_decimal_places: Optional[int] = None, fmt: Optional[str] = None, locale: Optional[str] = None, + **kwargs, ) -> str: """Render a number with optional formatting options. @@ -1065,14 +1066,17 @@ def format_datetime( timezone: Optional[str] = None, fmt: Optional[str] = None, locale: Optional[str] = None, + date_format: str = 'medium', + **kwargs, ): """Format a datetime object for display. Arguments: dt: The datetime object to format timezone: The timezone to use for the date (defaults to the server timezone) - fmt: The strftime format string to use. When provided, takes priority over locale. + fmt: The strftime format string to use. When provided, takes priority over locale and date_format. locale: Optional locale override (e.g. 'en-us', 'de-de'). Used for locale-aware formatting when no fmt is given. + date_format: Babel date format style. One of 'full', 'long', 'medium' (default), 'short'. """ check_nulls('format_datetime', dt) @@ -1081,10 +1085,7 @@ def format_datetime( if fmt: return dt.strftime(fmt) - return babel_format_datetime(dt, format='medium', locale=get_locale(locale)) - - # Final backup - return an ISO formatted string - return dt.isoformat() + return babel_format_datetime(dt, format=date_format, locale=get_locale(locale)) @register.simple_tag @@ -1093,14 +1094,17 @@ def format_date( timezone: Optional[str] = None, fmt: Optional[str] = None, locale: Optional[str] = None, + date_format: str = 'medium', + **kwargs, ): """Format a date object for display. Arguments: dt: The date to format timezone: The timezone to use for the date (defaults to the server timezone) - fmt: The strftime format string to use. When provided, takes priority over locale. + fmt: The strftime format string to use. When provided, takes priority over locale and date_format. locale: Optional locale override (e.g. 'en-us', 'de-de'). Used for locale-aware formatting when no fmt is given. + date_format: Babel date format style. One of 'full', 'long', 'medium' (default), 'short'. """ check_nulls('format_date', dt) @@ -1112,7 +1116,7 @@ def format_date( if fmt: return dt.strftime(fmt) - return babel_format_date(dt, format='medium', locale=get_locale(locale)) + return babel_format_date(dt, format=date_format, locale=get_locale(locale)) @register.simple_tag() diff --git a/src/backend/InvenTree/report/test_tags.py b/src/backend/InvenTree/report/test_tags.py index 3e40ae0a65..04c3ca9295 100644 --- a/src/backend/InvenTree/report/test_tags.py +++ b/src/backend/InvenTree/report/test_tags.py @@ -730,6 +730,25 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): with override_settings(LANGUAGE_CODE='en-us'): self.assertEqual(report_tags.format_date(dt), 'Mar 13, 2024') + # date_format controls the Babel style + self.assertEqual( + report_tags.format_date(dt, locale='en-us', date_format='short'), '3/13/24' + ) + self.assertEqual( + report_tags.format_date(dt, locale='en-us', date_format='long'), + 'March 13, 2024', + ) + self.assertEqual( + report_tags.format_date(dt, locale='en-us', date_format='full'), + 'Wednesday, March 13, 2024', + ) + + # fmt= wins over date_format= + self.assertEqual( + report_tags.format_date(dt, fmt='%Y', locale='en-us', date_format='full'), + '2024', + ) + # Invalid locale raises ValidationError with self.assertRaises(ValidationError): report_tags.format_date(dt, locale='xx-zz') @@ -758,6 +777,24 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): with override_settings(LANGUAGE_CODE='de-de'): self.assertEqual(report_tags.format_datetime(dt), '19.06.2026, 15:30:00') + # date_format controls the Babel style + self.assertEqual( + report_tags.format_datetime(dt, locale='de-de', date_format='short'), + '19.06.26, 15:30', + ) + self.assertEqual( + report_tags.format_datetime(dt, locale='de-de', date_format='long'), + '19. Juni 2026 um 15:30:00 UTC', # codespell:ignore "Juni" + ) + + # fmt= wins over date_format= + self.assertEqual( + report_tags.format_datetime( + dt, fmt='%H:%M', locale='en-us', date_format='full' + ), + '15:30', + ) + # Invalid locale raises ValidationError with self.assertRaises(ValidationError): report_tags.format_datetime(dt, locale='xx-zz')