2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 03:25:42 +00:00

Format number (#8482)

* Add extra options for 'format_number' helper

* Update documentation

* Improved typing hints and docs cleanup

* Fix link
This commit is contained in:
Oliver
2024-11-14 16:11:01 +11:00
committed by GitHub
parent ae88124294
commit da112211e5
7 changed files with 193 additions and 78 deletions

View File

@ -3,6 +3,7 @@
import io
import logging
from decimal import Decimal
from typing import Optional
from urllib.parse import urljoin
from django.conf import settings
@ -179,12 +180,12 @@ def download_image_from_url(remote_url, timeout=2.5):
def render_currency(
money,
decimal_places=None,
currency=None,
min_decimal_places=None,
max_decimal_places=None,
include_symbol=True,
money: Money,
decimal_places: Optional[int] = None,
currency: Optional[str] = None,
min_decimal_places: Optional[int] = None,
max_decimal_places: Optional[int] = None,
include_symbol: bool = True,
):
"""Render a currency / Money object to a formatted string (e.g. for reports).

View File

@ -3,7 +3,9 @@
import base64
import logging
import os
from datetime import date, datetime
from decimal import Decimal
from typing import Any, Optional
from django import template
from django.conf import settings
@ -28,7 +30,7 @@ logger = logging.getLogger('inventree')
@register.simple_tag()
def getindex(container: list, index: int):
def getindex(container: list, index: int) -> Any:
"""Return the value contained at the specified index of the list.
This function is provideed to get around template rendering limitations.
@ -55,7 +57,7 @@ def getindex(container: list, index: int):
@register.simple_tag()
def getkey(container: dict, key):
def getkey(container: dict, key: str) -> Any:
"""Perform key lookup in the provided dict object.
This function is provided to get around template rendering limitations.
@ -82,7 +84,7 @@ def asset(filename):
filename: Asset filename (relative to the 'assets' media directory)
Raises:
FileNotFoundError if file does not exist
FileNotFoundError: If file does not exist
"""
if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness'
@ -104,30 +106,31 @@ def asset(filename):
@register.simple_tag()
def uploaded_image(
filename,
replace_missing=True,
replacement_file='blank_image.png',
validate=True,
filename: str,
replace_missing: bool = True,
replacement_file: str = 'blank_image.png',
validate: bool = True,
width: Optional[int] = None,
height: Optional[int] = None,
rotate: Optional[float] = None,
**kwargs,
):
) -> str:
"""Return raw image data from an 'uploaded' image.
Arguments:
filename: The filename of the image relative to the MEDIA_ROOT directory
replace_missing: Optionally return a placeholder image if the provided filename does not exist (default = True)
replacement_file: The filename of the placeholder image (default = 'blank_image.png')
validate: Optionally validate that the file is a valid image file (default = True)
kwargs:
width: Optional width of the image (default = None)
height: Optional height of the image (default = None)
validate: Optionally validate that the file is a valid image file
width: Optional width of the image
height: Optional height of the image
rotate: Optional rotation to apply to the image
Returns:
Binary image data to be rendered directly in a <img> tag
Raises:
FileNotFoundError if the file does not exist
FileNotFoundError: If the file does not exist
"""
if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness'
@ -169,9 +172,6 @@ def uploaded_image(
# A placeholder image showing that the image is missing
img = Image.new('RGB', (64, 64), color='red')
width = kwargs.get('width')
height = kwargs.get('height')
if width is not None:
try:
width = int(width)
@ -199,7 +199,7 @@ def uploaded_image(
img = img.resize((wsize, height))
# Optionally rotate the image
if rotate := kwargs.get('rotate'):
if rotate is not None:
try:
rotate = int(rotate)
img = img.rotate(rotate)
@ -213,7 +213,7 @@ def uploaded_image(
@register.simple_tag()
def encode_svg_image(filename):
def encode_svg_image(filename: str) -> str:
"""Return a base64-encoded svg image data string."""
if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness'
@ -243,7 +243,7 @@ def encode_svg_image(filename):
@register.simple_tag()
def part_image(part: Part, preview=False, thumbnail=False, **kwargs):
def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwargs):
"""Return a fully-qualified path for a part image.
Arguments:
@ -252,7 +252,7 @@ def part_image(part: Part, preview=False, thumbnail=False, **kwargs):
thumbnail: Return the thumbnail image (default = False)
Raises:
TypeError if provided part is not a Part instance
TypeError: If provided part is not a Part instance
"""
if type(part) is not Part:
raise TypeError(_('part_image tag requires a Part instance'))
@ -268,7 +268,7 @@ def part_image(part: Part, preview=False, thumbnail=False, **kwargs):
@register.simple_tag()
def part_parameter(part: Part, parameter_name: str):
def part_parameter(part: Part, parameter_name: str) -> str:
"""Return a PartParameter object for the given part and parameter name.
Arguments:
@ -284,7 +284,9 @@ def part_parameter(part: Part, parameter_name: str):
@register.simple_tag()
def company_image(company, preview=False, thumbnail=False, **kwargs):
def company_image(
company: Company, preview: bool = False, thumbnail: bool = False, **kwargs
) -> str:
"""Return a fully-qualified path for a company image.
Arguments:
@ -293,7 +295,7 @@ def company_image(company, preview=False, thumbnail=False, **kwargs):
thumbnail: Return the thumbnail image (default = False)
Raises:
TypeError if provided company is not a Company instance
TypeError: If provided company is not a Company instance
"""
if type(company) is not Company:
raise TypeError(_('company_image tag requires a Company instance'))
@ -309,7 +311,7 @@ def company_image(company, preview=False, thumbnail=False, **kwargs):
@register.simple_tag()
def logo_image(**kwargs):
def logo_image(**kwargs) -> str:
"""Return a fully-qualified path for the logo image.
- If a custom logo has been provided, return a path to that logo
@ -322,7 +324,7 @@ def logo_image(**kwargs):
@register.simple_tag()
def internal_link(link, text):
def internal_link(link, text) -> str:
"""Make a <a></a> href which points to an InvenTree URL.
Uses the InvenTree.helpers_model.construct_absolute_url function to build the URL.
@ -396,13 +398,20 @@ def render_html_text(text: str, **kwargs):
@register.simple_tag
def format_number(number, **kwargs):
def format_number(
number,
decimal_places: Optional[int] = None,
integer: bool = False,
leading: int = 0,
separator: Optional[str] = None,
) -> str:
"""Render a number with optional formatting options.
kwargs:
Arguments:
decimal_places: Number of decimal places to render
integer: Boolean, whether to render the number as an integer
leading: Number of leading zeros
leading: Number of leading zeros (default = 0)
separator: Character to use as a thousands separator (default = None)
"""
try:
number = Decimal(str(number))
@ -410,28 +419,30 @@ def format_number(number, **kwargs):
# If the number cannot be converted to a Decimal, just return the original value
return str(number)
if kwargs.get('integer', False):
if integer:
# Convert to integer
number = Decimal(int(number))
# Normalize the number (remove trailing zeroes)
number = number.normalize()
decimals = kwargs.get('decimal_places')
decimal_places
if decimals is not None:
if decimal_places is not None:
try:
decimals = int(decimals)
number = round(number, decimals)
decimal_places = int(decimal_places)
number = round(number, decimal_places)
except ValueError:
pass
# Re-encode, and normalize again
value = Decimal(number).normalize()
value = format(value, 'f')
value = str(value)
leading = kwargs.get('leading')
if separator:
value = f'{value:,}'
value = value.replace(',', separator)
else:
value = f'{value}'
if leading is not None:
try:
@ -444,37 +455,39 @@ def format_number(number, **kwargs):
@register.simple_tag
def format_datetime(datetime, timezone=None, fmt=None):
def format_datetime(
dt: datetime, timezone: Optional[str] = None, fmt: Optional[str] = None
):
"""Format a datetime object for display.
Arguments:
datetime: The datetime object to format
dt: The datetime object to format
timezone: The timezone to use for the date (defaults to the server timezone)
fmt: The format string to use (defaults to ISO formatting)
"""
datetime = InvenTree.helpers.to_local_time(datetime, timezone)
dt = InvenTree.helpers.to_local_time(dt, timezone)
if fmt:
return datetime.strftime(fmt)
return dt.strftime(fmt)
else:
return datetime.isoformat()
return dt.isoformat()
@register.simple_tag
def format_date(date, timezone=None, fmt=None):
def format_date(dt: date, timezone: Optional[str] = None, fmt: Optional[str] = None):
"""Format a date object for display.
Arguments:
date: The date to format
dt: The date to format
timezone: The timezone to use for the date (defaults to the server timezone)
fmt: The format string to use (defaults to ISO formatting)
"""
date = InvenTree.helpers.to_local_time(date, timezone).date()
dt = InvenTree.helpers.to_local_time(dt, timezone).date()
if fmt:
return date.strftime(fmt)
return dt.strftime(fmt)
else:
return date.isoformat()
return dt.isoformat()
@register.simple_tag()

View File

@ -156,6 +156,18 @@ class ReportTagTest(TestCase):
self.assertEqual(report_tags.multiply(2.3, 4), 9.2)
self.assertEqual(report_tags.divide(100, 5), 20)
def test_number_tags(self):
"""Simple tests for number formatting tags."""
fn = report_tags.format_number
self.assertEqual(fn(1234), '1234')
self.assertEqual(fn(1234.5678, decimal_places=2), '1234.57')
self.assertEqual(fn(1234.5678, decimal_places=3), '1234.568')
self.assertEqual(fn(-9999.5678, decimal_places=2, separator=','), '-9,999.57')
self.assertEqual(
fn(9988776655.4321, integer=True, separator=' '), '9 988 776 655'
)
@override_settings(TIME_ZONE='America/New_York')
def test_date_tags(self):
"""Test for date formatting tags.