2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 193 additions and 78 deletions

View File

@ -7,7 +7,7 @@ title: Machines
InvenTree has a builtin machine registry. There are different machine types available where each type can have different drivers. Drivers and even custom machine types can be provided by plugins.
!!! info "Requires Redis"
If the machines features is used in production setup using workers, a shared [redis cache](../../start/docker.md#redis-cache) is required to function properly.
If the machines features is used in production setup using workers, a shared [redis cache](../../start/processes.md#cache-server) is required to function properly.
### Registry

View File

@ -41,6 +41,13 @@ A number of helper functions are available for accessing data contained in a par
To return the element at a given index in a container which supports indexed access (such as a [list](https://www.w3schools.com/python/python_lists.asp)), use the `getindex` function:
::: report.templatetags.report.getindex
options:
show_docstring_description: false
show_source: False
#### Example
```html
{% raw %}
{% getindex my_list 1 as value %}
@ -53,6 +60,13 @@ Item: {{ value }}
To return an element corresponding to a certain key in a container which supports key access (such as a [dictionary](https://www.w3schools.com/python/python_dictionaries.asp)), use the `getkey` function:
::: report.templatetags.report.getkey
options:
show_docstring_description: false
show_source: False
#### Example
```html
{% raw %}
<ul>
@ -66,8 +80,17 @@ To return an element corresponding to a certain key in a container which support
## Number Formatting
### format_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:
::: report.templatetags.report.format_number
options:
show_docstring_description: false
show_source: False
#### Example
```html
{% raw %}
{% load report %}
@ -82,15 +105,24 @@ The helper function `format_number` allows for some common number formatting opt
For rendering date and datetime information, the following helper functions are available:
- `format_date`: Format a date object
- `format_datetime`: Format a datetime object
### format_date
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:
::: report.templatetags.report.format_date
options:
show_docstring_description: false
show_source: False
### format_datetime
::: report.templatetags.report.format_datetime
options:
show_docstring_description: false
show_source: False
### Date Formatting
If not specified, these methods return a result which uses ISO formatting. Refer to the [datetime format codes](https://docs.python.org/3/library/datetime.html#format-codes) for more information! |
| 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
@ -106,8 +138,18 @@ Datetime: {% format_datetime my_datetime format="%d-%m-%Y %H:%M%S" %}
## Currency Formatting
### render_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:
::: InvenTree.helpers_model.render_currency
options:
show_docstring_description: false
show_source: False
#### Example
```html
{% raw %}
{% load report %}
@ -124,20 +166,40 @@ Total Price: {% render_currency order.total_price currency='NZD' decimal_places=
{% endraw %}
```
The following keyword arguments are available to the `render_currency` function:
| Argument | Description |
| --- | --- |
| currency | Specify the currency code to render in (will attempt conversion if different to provided currency) |
| decimal_places | Specify the number of decimal places to render |
| min_decimal_places | Specify the minimum number of decimal places to render |
| max_decimal_places | Specify the maximum number of decimal places to render |
| include_symbol | Include currency symbol in rendered value (default = True) |
## Maths Operations
Simple mathematical operators are available, as demonstrated in the example template below:
### add
::: report.templatetags.report.add
options:
show_docstring_description: false
show_source: False
### subtract
::: report.templatetags.report.subtract
options:
show_docstring_description: false
show_source: False
### multiply
::: report.templatetags.report.multiply
options:
show_docstring_description: false
show_source: False
### divide
::: report.templatetags.report.divide
options:
show_docstring_description: false
show_source: False
### Example
```html
{% raw %}
<!-- Load the report helper functions -->
@ -170,10 +232,15 @@ Total: {% multiply line.purchase_price line.quantity %}<br>
*Media files* are any files uploaded to the InvenTree server by the user. These are stored under the `/media/` directory and can be accessed for use in custom reports or labels.
### Uploaded Images
### uploaded_image
You can access an uploaded image file if you know the *path* of the image, relative to the top-level `/media/` directory. To load the image into a report, use the `{% raw %}{% uploaded_image ... %}{% endraw %}` tag:
::: report.templatetags.report.uploaded_image
options:
show_docstring_description: false
show_source: False
```html
{% raw %}
<!-- Load the report helper functions -->
@ -199,7 +266,12 @@ The `{% raw %}{% uploaded_image %}{% endraw %}` tag supports some optional param
{% endraw %}```
### SVG Images
### encode_svg_image
::: report.templatetags.report.encode_svg_image
options:
show_docstring_description: false
show_source: False
SVG images need to be handled in a slightly different manner. When embedding an uploaded SVG image, use the `{% raw %}{% encode_svg_image ... %}{% endraw %}` tag:
@ -211,10 +283,15 @@ SVG images need to be handled in a slightly different manner. When embedding an
{% endraw %}
```
### Part images
### part_image
A shortcut function is provided for rendering an image associated with a Part instance. You can render the image of the part using the `{% raw %}{% part_image ... %}{% endraw %}` template tag:
::: report.templatetags.report.part_image
options:
show_docstring_description: false
show_source: False
```html
{% raw %}
<!-- Load the report helper functions -->
@ -225,7 +302,7 @@ A shortcut function is provided for rendering an image associated with a Part in
#### Image Arguments
Any optional arguments which can be used in the [uploaded_image tag](#uploaded-images) can be used here too.
Any optional arguments which can be used in the [uploaded_image tag](#uploaded_image) can be used here too.
#### Image Variations
@ -243,10 +320,15 @@ The *Part* model supports *preview* (256 x 256) and *thumbnail* (128 x 128) vers
```
### Company Images
### company_image
A shortcut function is provided for rendering an image associated with a Company instance. You can render the image of the company using the `{% raw %}{% company_image ... %}{% endraw %}` template tag:
::: report.templatetags.report.company_image
options:
show_docstring_description: false
show_source: False
```html
{% raw %}
<!-- Load the report helper functions -->
@ -326,7 +408,14 @@ You can add asset images to the reports and labels by using the `{% raw %}{% ass
## Part Parameters
If you need to load a part parameter for a particular Part, within the context of your template, you can use the `part_parameter` template tag.
If you need to load a part parameter for a particular Part, within the context of your template, you can use the `part_parameter` template tag:
::: report.templatetags.report.part_parameter
options:
show_docstring_description: false
show_source: False
### Example
The following example assumes that you have a report or label which contains a valid [Part](../part/part.md) instance:

View File

@ -177,7 +177,7 @@ Asset files can be rendered directly into the template as follows
If the requested asset name does not match the name of an uploaded asset, the template will continue without loading the image.
!!! info "Assets location"
You need to ensure your asset images to the report/assets directory in the [data directory](../start/intro.md#file-storage). Upload new assets via the [admin interface](../settings/admin.md) to ensure they are uploaded to the correct location on the server.
Upload new assets via the [admin interface](../settings/admin.md) to ensure they are uploaded to the correct location on the server.
## Report Snippets

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.