mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Report: Add date rendering (#6706)
* Validate timezone in settings.py * Add helper functions for timezone information - Extract server timezone - Convert provided time to specified timezone * Add more unit tests * Remove debug print * Test fix * Add report helper tags - format_date - format_datetime - Update report templates - Unit tests * Add setting to control report errors - Only log errors to DB if setting is enabled * Update example report * Fixes for to_local_time * Update type hinting * Fix unit test typo
This commit is contained in:
		@@ -1,5 +1,6 @@
 | 
				
			|||||||
"""Provides helper functions used throughout the InvenTree project."""
 | 
					"""Provides helper functions used throughout the InvenTree project."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import datetime
 | 
				
			||||||
import hashlib
 | 
					import hashlib
 | 
				
			||||||
import io
 | 
					import io
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
@@ -11,6 +12,7 @@ from decimal import Decimal, InvalidOperation
 | 
				
			|||||||
from typing import TypeVar
 | 
					from typing import TypeVar
 | 
				
			||||||
from wsgiref.util import FileWrapper
 | 
					from wsgiref.util import FileWrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.utils.timezone as timezone
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.contrib.staticfiles.storage import StaticFilesStorage
 | 
					from django.contrib.staticfiles.storage import StaticFilesStorage
 | 
				
			||||||
from django.core.exceptions import FieldError, ValidationError
 | 
					from django.core.exceptions import FieldError, ValidationError
 | 
				
			||||||
@@ -18,6 +20,7 @@ from django.core.files.storage import default_storage
 | 
				
			|||||||
from django.http import StreamingHttpResponse
 | 
					from django.http import StreamingHttpResponse
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytz
 | 
				
			||||||
import regex
 | 
					import regex
 | 
				
			||||||
from bleach import clean
 | 
					from bleach import clean
 | 
				
			||||||
from djmoney.money import Money
 | 
					from djmoney.money import Money
 | 
				
			||||||
@@ -863,6 +866,56 @@ def hash_file(filename: str):
 | 
				
			|||||||
    return hashlib.md5(open(filename, 'rb').read()).hexdigest()
 | 
					    return hashlib.md5(open(filename, 'rb').read()).hexdigest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def server_timezone() -> str:
 | 
				
			||||||
 | 
					    """Return the timezone of the server as a string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    e.g. "UTC" / "Australia/Sydney" etc
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return settings.TIME_ZONE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def to_local_time(time, target_tz: str = None):
 | 
				
			||||||
 | 
					    """Convert the provided time object to the local timezone.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Arguments:
 | 
				
			||||||
 | 
					        time: The time / date to convert
 | 
				
			||||||
 | 
					        target_tz: The desired timezone (string) - defaults to server time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        A timezone aware datetime object, with the desired timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Raises:
 | 
				
			||||||
 | 
					        TypeError: If the provided time object is not a datetime or date object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if isinstance(time, datetime.datetime):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    elif isinstance(time, datetime.date):
 | 
				
			||||||
 | 
					        time = timezone.datetime(year=time.year, month=time.month, day=time.day)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        raise TypeError(
 | 
				
			||||||
 | 
					            f'Argument must be a datetime or date object (found {type(time)}'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Extract timezone information from the provided time
 | 
				
			||||||
 | 
					    source_tz = getattr(time, 'tzinfo', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not source_tz:
 | 
				
			||||||
 | 
					        # Default to UTC if not provided
 | 
				
			||||||
 | 
					        source_tz = pytz.utc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not target_tz:
 | 
				
			||||||
 | 
					        target_tz = server_timezone()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        target_tz = pytz.timezone(str(target_tz))
 | 
				
			||||||
 | 
					    except pytz.UnknownTimeZoneError:
 | 
				
			||||||
 | 
					        target_tz = pytz.utc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    target_time = time.replace(tzinfo=source_tz).astimezone(target_tz)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return target_time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_objectreference(
 | 
					def get_objectreference(
 | 
				
			||||||
    obj, type_ref: str = 'content_type', object_ref: str = 'object_id'
 | 
					    obj, type_ref: str = 'content_type', object_ref: str = 'object_id'
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ from django.http import Http404
 | 
				
			|||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import moneyed
 | 
					import moneyed
 | 
				
			||||||
 | 
					import pytz
 | 
				
			||||||
from dotenv import load_dotenv
 | 
					from dotenv import load_dotenv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from InvenTree.config import get_boolean_setting, get_custom_file, get_setting
 | 
					from InvenTree.config import get_boolean_setting, get_custom_file, get_setting
 | 
				
			||||||
@@ -938,8 +939,13 @@ LOCALE_PATHS = (BASE_DIR.joinpath('locale/'),)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
TIME_ZONE = get_setting('INVENTREE_TIMEZONE', 'timezone', 'UTC')
 | 
					TIME_ZONE = get_setting('INVENTREE_TIMEZONE', 'timezone', 'UTC')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
USE_I18N = True
 | 
					# Check that the timezone is valid
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    pytz.timezone(TIME_ZONE)
 | 
				
			||||||
 | 
					except pytz.exceptions.UnknownTimeZoneError:  # pragma: no cover
 | 
				
			||||||
 | 
					    raise ValueError(f"Specified timezone '{TIME_ZONE}' is not valid")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USE_I18N = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Do not use native timezone support in "test" mode
 | 
					# Do not use native timezone support in "test" mode
 | 
				
			||||||
# It generates a *lot* of cruft in the logs
 | 
					# It generates a *lot* of cruft in the logs
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,8 +14,10 @@ from django.core import mail
 | 
				
			|||||||
from django.core.exceptions import ValidationError
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
from django.test import TestCase, override_settings, tag
 | 
					from django.test import TestCase, override_settings, tag
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pint.errors
 | 
					import pint.errors
 | 
				
			||||||
 | 
					import pytz
 | 
				
			||||||
from djmoney.contrib.exchange.exceptions import MissingRate
 | 
					from djmoney.contrib.exchange.exceptions import MissingRate
 | 
				
			||||||
from djmoney.contrib.exchange.models import Rate, convert_money
 | 
					from djmoney.contrib.exchange.models import Rate, convert_money
 | 
				
			||||||
from djmoney.money import Money
 | 
					from djmoney.money import Money
 | 
				
			||||||
@@ -746,6 +748,47 @@ class TestHelpers(TestCase):
 | 
				
			|||||||
            self.assertEqual(helpers.generateTestKey(name), key)
 | 
					            self.assertEqual(helpers.generateTestKey(name), key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestTimeFormat(TestCase):
 | 
				
			||||||
 | 
					    """Unit test for time formatting functionality."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(TIME_ZONE='UTC')
 | 
				
			||||||
 | 
					    def test_tz_utc(self):
 | 
				
			||||||
 | 
					        """Check UTC timezone."""
 | 
				
			||||||
 | 
					        self.assertEqual(InvenTree.helpers.server_timezone(), 'UTC')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(TIME_ZONE='Europe/London')
 | 
				
			||||||
 | 
					    def test_tz_london(self):
 | 
				
			||||||
 | 
					        """Check London timezone."""
 | 
				
			||||||
 | 
					        self.assertEqual(InvenTree.helpers.server_timezone(), 'Europe/London')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(TIME_ZONE='Australia/Sydney')
 | 
				
			||||||
 | 
					    def test_to_local_time(self):
 | 
				
			||||||
 | 
					        """Test that the local time conversion works as expected."""
 | 
				
			||||||
 | 
					        source_time = timezone.datetime(
 | 
				
			||||||
 | 
					            year=2000,
 | 
				
			||||||
 | 
					            month=1,
 | 
				
			||||||
 | 
					            day=1,
 | 
				
			||||||
 | 
					            hour=0,
 | 
				
			||||||
 | 
					            minute=0,
 | 
				
			||||||
 | 
					            second=0,
 | 
				
			||||||
 | 
					            tzinfo=pytz.timezone('Europe/London'),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tests = [
 | 
				
			||||||
 | 
					            ('UTC', '2000-01-01 00:01:00+00:00'),
 | 
				
			||||||
 | 
					            ('Europe/London', '2000-01-01 00:00:00-00:01'),
 | 
				
			||||||
 | 
					            ('America/New_York', '1999-12-31 19:01:00-05:00'),
 | 
				
			||||||
 | 
					            # All following tests should result in the same value
 | 
				
			||||||
 | 
					            ('Australia/Sydney', '2000-01-01 11:01:00+11:00'),
 | 
				
			||||||
 | 
					            (None, '2000-01-01 11:01:00+11:00'),
 | 
				
			||||||
 | 
					            ('', '2000-01-01 11:01:00+11:00'),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for tz, expected in tests:
 | 
				
			||||||
 | 
					            local_time = InvenTree.helpers.to_local_time(source_time, tz)
 | 
				
			||||||
 | 
					            self.assertEqual(str(local_time), expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestQuoteWrap(TestCase):
 | 
					class TestQuoteWrap(TestCase):
 | 
				
			||||||
    """Tests for string wrapping."""
 | 
					    """Tests for string wrapping."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1653,6 +1653,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
 | 
				
			|||||||
            'default': False,
 | 
					            'default': False,
 | 
				
			||||||
            'validator': bool,
 | 
					            'validator': bool,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        'REPORT_LOG_ERRORS': {
 | 
				
			||||||
 | 
					            'name': _('Log Report Errors'),
 | 
				
			||||||
 | 
					            'description': _('Log errors which occur when generating reports'),
 | 
				
			||||||
 | 
					            'default': False,
 | 
				
			||||||
 | 
					            'validator': bool,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        'REPORT_DEFAULT_PAGE_SIZE': {
 | 
					        'REPORT_DEFAULT_PAGE_SIZE': {
 | 
				
			||||||
            'name': _('Page Size'),
 | 
					            'name': _('Page Size'),
 | 
				
			||||||
            'description': _('Default page size for PDF reports'),
 | 
					            'description': _('Default page size for PDF reports'),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -264,6 +264,11 @@ class ReportPrintMixin:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        except Exception as exc:
 | 
					        except Exception as exc:
 | 
				
			||||||
            # Log the exception to the database
 | 
					            # Log the exception to the database
 | 
				
			||||||
 | 
					            if InvenTree.helpers.str2bool(
 | 
				
			||||||
 | 
					                common.models.InvenTreeSetting.get_setting(
 | 
				
			||||||
 | 
					                    'REPORT_LOG_ERRORS', cache=False
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
                log_error(request.path)
 | 
					                log_error(request.path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Re-throw the exception to the client as a DRF exception
 | 
					            # Re-throw the exception to the client as a DRF exception
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ margin-top: 4cm;
 | 
				
			|||||||
{% endblock page_margin %}
 | 
					{% endblock page_margin %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_left %}
 | 
					{% block bottom_left %}
 | 
				
			||||||
content: "v{{ report_revision }} - {{ date.isoformat }}";
 | 
					content: "v{{ report_revision }} - {% format_date date %}";
 | 
				
			||||||
{% endblock bottom_left %}
 | 
					{% endblock bottom_left %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_center %}
 | 
					{% block bottom_center %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,7 +74,7 @@ margin-top: 4cm;
 | 
				
			|||||||
{% endblock style %}
 | 
					{% endblock style %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_left %}
 | 
					{% block bottom_left %}
 | 
				
			||||||
content: "v{{ report_revision }} - {{ date.isoformat }}";
 | 
					content: "v{{ report_revision }} - {% format_date date %}";
 | 
				
			||||||
{% endblock bottom_left %}
 | 
					{% endblock bottom_left %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header_content %}
 | 
					{% block header_content %}
 | 
				
			||||||
@@ -119,13 +119,13 @@ content: "v{{ report_revision }} - {{ date.isoformat }}";
 | 
				
			|||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <th>{% trans "Issued" %}</th>
 | 
					                <th>{% trans "Issued" %}</th>
 | 
				
			||||||
                <td>{% render_date build.creation_date %}</td>
 | 
					                <td>{% format_date build.creation_date %}</td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <th>{% trans "Target Date" %}</th>
 | 
					                <th>{% trans "Target Date" %}</th>
 | 
				
			||||||
                <td>
 | 
					                <td>
 | 
				
			||||||
                    {% if build.target_date %}
 | 
					                    {% if build.target_date %}
 | 
				
			||||||
                    {% render_date build.target_date %}
 | 
					                    {% format_date build.target_date %}
 | 
				
			||||||
                    {% else %}
 | 
					                    {% else %}
 | 
				
			||||||
                    <em>Not specified</em>
 | 
					                    <em>Not specified</em>
 | 
				
			||||||
                    {% endif %}
 | 
					                    {% endif %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ margin-top: 4cm;
 | 
				
			|||||||
{% endblock page_margin %}
 | 
					{% endblock page_margin %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_left %}
 | 
					{% block bottom_left %}
 | 
				
			||||||
content: "v{{ report_revision }} - {{ date.isoformat }}";
 | 
					content: "v{{ report_revision }} - {% format_date date %}";
 | 
				
			||||||
{% endblock bottom_left %}
 | 
					{% endblock bottom_left %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_center %}
 | 
					{% block bottom_center %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ margin-top: 4cm;
 | 
				
			|||||||
{% endblock page_margin %}
 | 
					{% endblock page_margin %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_left %}
 | 
					{% block bottom_left %}
 | 
				
			||||||
content: "v{{ report_revision }} - {{ date.isoformat }}";
 | 
					content: "v{{ report_revision }} - {% format_date date %}";
 | 
				
			||||||
{% endblock bottom_left %}
 | 
					{% endblock bottom_left %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_center %}
 | 
					{% block bottom_center %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_left %}
 | 
					{% block bottom_left %}
 | 
				
			||||||
content: "{{ date.isoformat }}";
 | 
					content: "{% format_date date %}";
 | 
				
			||||||
{% endblock bottom_left %}
 | 
					{% endblock bottom_left %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_center %}
 | 
					{% block bottom_center %}
 | 
				
			||||||
@@ -133,7 +133,7 @@ content: "{% trans 'Stock Item Test Report' %}";
 | 
				
			|||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
            <td>{{ test_result.value }}</td>
 | 
					            <td>{{ test_result.value }}</td>
 | 
				
			||||||
            <td>{{ test_result.user.username }}</td>
 | 
					            <td>{{ test_result.user.username }}</td>
 | 
				
			||||||
            <td>{{ test_result.date.date.isoformat }}</td>
 | 
					            <td>{% format_date test_result.date.date %}</td>
 | 
				
			||||||
            {% else %}
 | 
					            {% else %}
 | 
				
			||||||
            {% if test_template.required %}
 | 
					            {% if test_template.required %}
 | 
				
			||||||
            <td colspan='4' class='required-test-not-found'>{% trans "No result (required)" %}</td>
 | 
					            <td colspan='4' class='required-test-not-found'>{% trans "No result (required)" %}</td>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -422,3 +422,37 @@ def format_number(number, **kwargs):
 | 
				
			|||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return value
 | 
					    return value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@register.simple_tag
 | 
				
			||||||
 | 
					def format_datetime(datetime, timezone=None, format=None):
 | 
				
			||||||
 | 
					    """Format a datetime object for display.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Arguments:
 | 
				
			||||||
 | 
					        datetime: The datetime object to format
 | 
				
			||||||
 | 
					        timezone: The timezone to use for the date (defaults to the server timezone)
 | 
				
			||||||
 | 
					        format: The format string to use (defaults to ISO formatting)
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    datetime = InvenTree.helpers.to_local_time(datetime, timezone)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if format:
 | 
				
			||||||
 | 
					        return datetime.strftime(format)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return datetime.isoformat()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@register.simple_tag
 | 
				
			||||||
 | 
					def format_date(date, timezone=None, format=None):
 | 
				
			||||||
 | 
					    """Format a date object for display.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Arguments:
 | 
				
			||||||
 | 
					        date: The date to format
 | 
				
			||||||
 | 
					        timezone: The timezone to use for the date (defaults to the server timezone)
 | 
				
			||||||
 | 
					        format: The format string to use (defaults to ISO formatting)
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    date = InvenTree.helpers.to_local_time(date, timezone).date()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if format:
 | 
				
			||||||
 | 
					        return date.strftime(format)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return date.isoformat()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,10 +8,12 @@ from pathlib import Path
 | 
				
			|||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.http.response import StreamingHttpResponse
 | 
					from django.http.response import StreamingHttpResponse
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import TestCase, override_settings
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from django.utils import timezone
 | 
				
			||||||
from django.utils.safestring import SafeString
 | 
					from django.utils.safestring import SafeString
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytz
 | 
				
			||||||
from PIL import Image
 | 
					from PIL import Image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import report.models as report_models
 | 
					import report.models as report_models
 | 
				
			||||||
@@ -153,6 +155,37 @@ class ReportTagTest(TestCase):
 | 
				
			|||||||
        self.assertEqual(report_tags.multiply(2.3, 4), 9.2)
 | 
					        self.assertEqual(report_tags.multiply(2.3, 4), 9.2)
 | 
				
			||||||
        self.assertEqual(report_tags.divide(100, 5), 20)
 | 
					        self.assertEqual(report_tags.divide(100, 5), 20)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(TIME_ZONE='America/New_York')
 | 
				
			||||||
 | 
					    def test_date_tags(self):
 | 
				
			||||||
 | 
					        """Test for date formatting tags.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - Source timezone is Australia/Sydney
 | 
				
			||||||
 | 
					        - Server timezone is America/New York
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        time = timezone.datetime(
 | 
				
			||||||
 | 
					            year=2024,
 | 
				
			||||||
 | 
					            month=3,
 | 
				
			||||||
 | 
					            day=13,
 | 
				
			||||||
 | 
					            hour=12,
 | 
				
			||||||
 | 
					            minute=30,
 | 
				
			||||||
 | 
					            second=0,
 | 
				
			||||||
 | 
					            tzinfo=pytz.timezone('Australia/Sydney'),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Format a set of tests: timezone, format, expected
 | 
				
			||||||
 | 
					        tests = [
 | 
				
			||||||
 | 
					            (None, None, '2024-03-12T22:25:00-04:00'),
 | 
				
			||||||
 | 
					            (None, '%d-%m-%y', '12-03-24'),
 | 
				
			||||||
 | 
					            ('UTC', None, '2024-03-13T02:25:00+00:00'),
 | 
				
			||||||
 | 
					            ('UTC', '%d-%B-%Y', '13-March-2024'),
 | 
				
			||||||
 | 
					            ('Europe/Amsterdam', None, '2024-03-13T03:25:00+01:00'),
 | 
				
			||||||
 | 
					            ('Europe/Amsterdam', '%y-%m-%d %H:%M', '24-03-13 03:25'),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for tz, fmt, expected in tests:
 | 
				
			||||||
 | 
					            result = report_tags.format_datetime(time, tz, fmt)
 | 
				
			||||||
 | 
					            self.assertEqual(result, expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BarcodeTagTest(TestCase):
 | 
					class BarcodeTagTest(TestCase):
 | 
				
			||||||
    """Unit tests for the barcode template tags."""
 | 
					    """Unit tests for the barcode template tags."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@
 | 
				
			|||||||
        {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE" icon="fa-file-pdf" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE" icon="fa-file-pdf" %}
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" icon="fa-print" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" icon="fa-print" %}
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" icon="fa-laptop-code" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" icon="fa-laptop-code" %}
 | 
				
			||||||
 | 
					        {% include "InvenTree/settings/setting.html" with key="REPORT_LOG_ERRORS" icon="fa-exclamation-circle" %}
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" icon="fa-vial" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" icon="fa-vial" %}
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="REPORT_ATTACH_TEST_REPORT" icon="fa-file-upload" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="REPORT_ATTACH_TEST_REPORT" icon="fa-file-upload" %}
 | 
				
			||||||
    </tbody>
 | 
					    </tbody>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@ margin-top: 4cm;
 | 
				
			|||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_left %}
 | 
					{% block bottom_left %}
 | 
				
			||||||
content: "v{{report_revision}} - {{ date.isoformat }}";
 | 
					content: "v{{report_revision}} - {% format_date date %}";
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_center %}
 | 
					{% block bottom_center %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -186,7 +186,7 @@ margin-top: 4cm;
 | 
				
			|||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block bottom_left %}
 | 
					{% block bottom_left %}
 | 
				
			||||||
content: "v{{report_revision}} - {{ date.isoformat }}";
 | 
					content: "v{{report_revision}} - {% format_date date %}";
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header_content %}
 | 
					{% block header_content %}
 | 
				
			||||||
@@ -230,13 +230,13 @@ content: "v{{report_revision}} - {{ date.isoformat }}";
 | 
				
			|||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <th>{% trans "Issued" %}</th>
 | 
					                <th>{% trans "Issued" %}</th>
 | 
				
			||||||
                <td>{% render_date build.creation_date %}</td>
 | 
					                <td>{% format_date build.creation_date %}</td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <th>{% trans "Target Date" %}</th>
 | 
					                <th>{% trans "Target Date" %}</th>
 | 
				
			||||||
                <td>
 | 
					                <td>
 | 
				
			||||||
                    {% if build.target_date %}
 | 
					                    {% if build.target_date %}
 | 
				
			||||||
                    {% render_date build.target_date %}
 | 
					                    {% format_date build.target_date %}
 | 
				
			||||||
                    {% else %}
 | 
					                    {% else %}
 | 
				
			||||||
                    <em>Not specified</em>
 | 
					                    <em>Not specified</em>
 | 
				
			||||||
                    {% endif %}
 | 
					                    {% endif %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@ To return an element corresponding to a certain key in a container which support
 | 
				
			|||||||
{% endraw %}
 | 
					{% endraw %}
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Formatting Numbers
 | 
					## Number Formatting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The helper function `format_number` allows for some common number formatting options. It takes a number (or a number-like string) as an input, as well as some formatting arguments. It returns a *string* containing the formatted number:
 | 
					The helper function `format_number` allows for some common number formatting options. It takes a number (or a number-like string) as an input, as well as some formatting arguments. It returns a *string* containing the formatted number:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -78,7 +78,33 @@ The helper function `format_number` allows for some common number formatting opt
 | 
				
			|||||||
{% endraw %}
 | 
					{% endraw %}
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Rendering Currency
 | 
					## Date Formatting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For rendering date and datetime information, the following helper functions are available:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `format_date`: Format a date object
 | 
				
			||||||
 | 
					- `format_datetime`: Format a datetime object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each of these helper functions takes a date or datetime object as an input, and returns a *string* containing the formatted date or datetime. The following additional arguments are available:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Argument | Description |
 | 
				
			||||||
 | 
					| --- | --- |
 | 
				
			||||||
 | 
					| timezone | Specify the timezone to render the date in. If not specified, uses the InvenTree server timezone |
 | 
				
			||||||
 | 
					| format | Specify the format string to use for rendering the date. If not specified, uses ISO formatting. Refer to the [datetime format codes](https://docs.python.org/3/library/datetime.html#format-codes) for more information! |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Example
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A simple example of using the date formatting helper functions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```html
 | 
				
			||||||
 | 
					{% raw %}
 | 
				
			||||||
 | 
					{% load report %}
 | 
				
			||||||
 | 
					Date: {% format_date my_date timezone="Australia/Sydney" %}
 | 
				
			||||||
 | 
					Datetime: {% format_datetime my_datetime format="%d-%m-%Y %H:%M%S" %}
 | 
				
			||||||
 | 
					{% endraw %}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Currency Formatting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The helper function `render_currency` allows for simple rendering of currency data. This function can also convert the specified amount of currency into a different target currency:
 | 
					The helper function `render_currency` allows for simple rendering of currency data. This function can also convert the specified amount of currency into a different target currency:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -152,6 +152,7 @@ export default function SystemSettings() {
 | 
				
			|||||||
              'REPORT_ENABLE',
 | 
					              'REPORT_ENABLE',
 | 
				
			||||||
              'REPORT_DEFAULT_PAGE_SIZE',
 | 
					              'REPORT_DEFAULT_PAGE_SIZE',
 | 
				
			||||||
              'REPORT_DEBUG_MODE',
 | 
					              'REPORT_DEBUG_MODE',
 | 
				
			||||||
 | 
					              'REPORT_LOG_ERRORS',
 | 
				
			||||||
              'REPORT_ENABLE_TEST_REPORT',
 | 
					              'REPORT_ENABLE_TEST_REPORT',
 | 
				
			||||||
              'REPORT_ATTACH_TEST_REPORT'
 | 
					              'REPORT_ATTACH_TEST_REPORT'
 | 
				
			||||||
            ]}
 | 
					            ]}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user