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:
@ -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).
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
Reference in New Issue
Block a user