mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 03:26:45 +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:
parent
ae88124294
commit
da112211e5
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user