diff --git a/docs/docs/extend/machines/overview.md b/docs/docs/extend/machines/overview.md
index f132903cd3d2..1ec4cc26be04 100644
--- a/docs/docs/extend/machines/overview.md
+++ b/docs/docs/extend/machines/overview.md
@@ -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
diff --git a/docs/docs/report/helpers.md b/docs/docs/report/helpers.md
index 6dc90fb7a6ec..125d021016b2 100644
--- a/docs/docs/report/helpers.md
+++ b/docs/docs/report/helpers.md
@@ -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 %}
@@ -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 %}
@@ -170,10 +232,15 @@ Total: {% multiply line.purchase_price line.quantity %}
*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 %}
@@ -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 %}
@@ -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 %}
@@ -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:
diff --git a/docs/docs/report/templates.md b/docs/docs/report/templates.md
index 465a4e16a9af..9620de0179d2 100644
--- a/docs/docs/report/templates.md
+++ b/docs/docs/report/templates.md
@@ -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
diff --git a/docs/main.py b/docs/main.py
index 84b91f7ad8c1..97b28c98ccb6 100644
--- a/docs/main.py
+++ b/docs/main.py
@@ -151,17 +151,19 @@ def sourcefile(filename, branch=None, raw=False):
if not os.path.exists(file_path):
raise FileNotFoundError(f'Source file {filename} does not exist.')
- repo_url = get_repo_url(raw=raw)
-
- if raw:
- url = f'{repo_url}/{branch}/{filename}'
- else:
- url = f'{repo_url}/blob/{branch}/{filename}'
+ # Construct repo URL
+ repo_url = get_repo_url(raw=False)
+ url = f'{repo_url}/blob/{branch}/{filename}'
# Check that the URL exists before returning it
if not check_link(url):
raise FileNotFoundError(f'URL {url} does not exist.')
+ if raw:
+ # If requesting the 'raw' URL, take this into account here...
+ repo_url = get_repo_url(raw=True)
+ url = f'{repo_url}/{branch}/{filename}'
+
return url
@env.macro
diff --git a/src/backend/InvenTree/InvenTree/helpers_model.py b/src/backend/InvenTree/InvenTree/helpers_model.py
index 61e66d290019..7333abba8319 100644
--- a/src/backend/InvenTree/InvenTree/helpers_model.py
+++ b/src/backend/InvenTree/InvenTree/helpers_model.py
@@ -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).
diff --git a/src/backend/InvenTree/report/templatetags/__init__.py b/src/backend/InvenTree/report/templatetags/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py
index 3f27e2788136..9ca5db5935af 100644
--- a/src/backend/InvenTree/report/templatetags/report.py
+++ b/src/backend/InvenTree/report/templatetags/report.py
@@ -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 @@
@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 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 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()
diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py
index ce3ea556fc6a..2caf4e1206cd 100644
--- a/src/backend/InvenTree/report/tests.py
+++ b/src/backend/InvenTree/report/tests.py
@@ -156,6 +156,18 @@ def test_maths_tags(self):
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.
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index f0186ae37578..287e9ce7d7ef 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -894,12 +894,6 @@ export default function PartDetail() {
visible={part.building > 0}
key='in_production'
/>,
- ,