mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-21 06:10:28 -06:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0756a7006 | ||
|
|
14f35fef2b | ||
|
|
44c1046a43 | ||
|
|
d0b87a1a12 | ||
|
|
a4894d9f4a | ||
|
|
884851733a | ||
|
|
847322e474 | ||
|
|
e56d013e2e | ||
|
|
8c13ccf59e | ||
|
|
dca5fd69ec | ||
|
|
dce95d824a | ||
|
|
f23d405392 | ||
|
|
3fe04747d7 | ||
|
|
8ff4eddeb9 | ||
|
|
de2edc4ed6 | ||
|
|
343f63c6ba | ||
|
|
2bccbffe56 | ||
|
|
5af0e50b79 | ||
|
|
0ae9cdd39f | ||
|
|
7babef098a | ||
|
|
fab846e3cc | ||
|
|
d485c6796b | ||
|
|
5c94366bb5 | ||
|
|
cebad3d142 | ||
|
|
3659bbe389 | ||
|
|
933a5a5595 | ||
|
|
6c0f6e38d0 | ||
|
|
8c9a438e59 | ||
|
|
6e37f0cd8b | ||
|
|
1c6d25ce33 | ||
|
|
86111ad9b9 | ||
|
|
524e6ddf79 | ||
|
|
83be1b8a0f | ||
|
|
974c2737af | ||
|
|
0a0da7b65b | ||
|
|
b12bd3bb4b | ||
|
|
83be3cfa71 | ||
|
|
fda47ff6ee | ||
|
|
69676f308b | ||
|
|
178e3313f9 | ||
|
|
b0353fafbf |
@@ -83,6 +83,26 @@ For more information, refer to the installation guides:
|
|||||||
!!! warning "Invoke Update"
|
!!! warning "Invoke Update"
|
||||||
You must ensure that the `invoke update` command is performed *every time* you update InvenTree
|
You must ensure that the `invoke update` command is performed *every time* you update InvenTree
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
Before performing an update, check the release notes! Any *breaking changes* (changes which require user intervention) will be clearly noted.
|
||||||
|
|
||||||
|
### Cannot import name get_storage_class
|
||||||
|
|
||||||
|
When running an install or update, you may see an error similar to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ImportError: cannot import name 'get_storage_class' from 'django.core.files.storage'
|
||||||
|
```
|
||||||
|
|
||||||
|
In such a situation, it is likely that the automatic backup procedure is unable to run, as the required python packages are not yet installed or are unavailable.
|
||||||
|
|
||||||
|
To proceed in this case, you can skip the backup procedure by running the `invoke update` command with the `--skip-backup` flag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
invoke update --skip-backup
|
||||||
|
```
|
||||||
|
|
||||||
### Feature *x* does not work after update
|
### Feature *x* does not work after update
|
||||||
|
|
||||||
If a particular menu / item is not visible after updating InvenTree, or a certain function no longer seems to work, it may be due to your internet browser caching old versions of CSS and JavaScript files.
|
If a particular menu / item is not visible after updating InvenTree, or a certain function no longer seems to work, it may be due to your internet browser caching old versions of CSS and JavaScript files.
|
||||||
|
|||||||
49
docs/docs/start/accounts.md
Normal file
49
docs/docs/start/accounts.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: Account Management
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Accounts
|
||||||
|
|
||||||
|
By default, InvenTree does not ship with any user accounts. Configuring user accounts is the first step to login to the InvenTree server.
|
||||||
|
|
||||||
|
### Administrator Account
|
||||||
|
|
||||||
|
You can configure InvenTree to create an administrator account on the first run. This account will have full *superuser* access to the InvenTree server.
|
||||||
|
|
||||||
|
This account is created when you first run the InvenTree server instance. The username / password for this account can be configured in the configuration file, or environment variables.
|
||||||
|
|
||||||
|
!!! info "More Information"
|
||||||
|
For more information on configuring the administrator account, refer to the [configuration documentation](./config.md#administrator-account).
|
||||||
|
|
||||||
|
### Create Superuser
|
||||||
|
|
||||||
|
Another way to create an administrator account is to use the `superuser` command. This will create a new superuser account with the specified username and password.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
invoke superuser
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if you are running InvenTree in a Docker container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -rm -it inventree-server invoke superuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
|
||||||
|
Once you have created an administrator account, you can create and manage additional user accounts from the InvenTree web interface.
|
||||||
|
|
||||||
|
## Password Management
|
||||||
|
|
||||||
|
### Reset Password via Command Line
|
||||||
|
|
||||||
|
If a password has been lost, and other backup options (such as email recovery) are unavailable, the system administrator can reset the password for a user account from the command line.
|
||||||
|
|
||||||
|
Log into the machine running the InvenTree server, and run the following command (from the top-level source directory):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/backend/InvenTree
|
||||||
|
python ./manage.py changepassword <username>
|
||||||
|
```
|
||||||
|
|
||||||
|
The system will prompt you to enter a new password for the specified user account.
|
||||||
@@ -46,6 +46,14 @@ Environment variable settings generally use the `INVENTREE_` prefix, and are all
|
|||||||
!!! warning "Available Variables"
|
!!! warning "Available Variables"
|
||||||
Some configuration options cannot be set via environment variables. Refer to the documentation below.
|
Some configuration options cannot be set via environment variables. Refer to the documentation below.
|
||||||
|
|
||||||
|
#### List Values
|
||||||
|
|
||||||
|
To specify a list value in an environment variable, use a comma-separated list. For example, to specify a list of trusted origins:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
INVENTREE_TRUSTED_ORIGINS='https://inventree.example.com:8443,https://stock.example.com:8443'
|
||||||
|
```
|
||||||
|
|
||||||
## Basic Options
|
## Basic Options
|
||||||
|
|
||||||
The following basic options are available:
|
The following basic options are available:
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ The following files required for this setup are provided with the InvenTree sour
|
|||||||
|
|
||||||
Download these files to a directory on your local machine.
|
Download these files to a directory on your local machine.
|
||||||
|
|
||||||
|
!!! warning "File Extensions"
|
||||||
|
If your computer adds *.txt* extensions to any of the downloaded files, rename the file and remove the added extension before continuing!
|
||||||
|
|
||||||
!!! success "Working Directory"
|
!!! success "Working Directory"
|
||||||
This tutorial assumes you are working from a direction where all of these files are located.
|
This tutorial assumes you are working from a directory where all of these files are located.
|
||||||
|
|
||||||
!!! tip "No Source Required"
|
!!! tip "No Source Required"
|
||||||
For a production setup you do not need the InvenTree source code. Simply download the three required files from the links above!
|
For a production setup you do not need the InvenTree source code. Simply download the three required files from the links above!
|
||||||
|
|||||||
14
docs/main.py
14
docs/main.py
@@ -136,17 +136,19 @@ def define_env(env):
|
|||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
raise FileNotFoundError(f'Source file {filename} does not exist.')
|
raise FileNotFoundError(f'Source file {filename} does not exist.')
|
||||||
|
|
||||||
repo_url = get_repo_url(raw=raw)
|
# Construct repo URL
|
||||||
|
repo_url = get_repo_url(raw=False)
|
||||||
if raw:
|
url = f'{repo_url}/blob/{branch}/{filename}'
|
||||||
url = f'{repo_url}/{branch}/{filename}'
|
|
||||||
else:
|
|
||||||
url = f'{repo_url}/blob/{branch}/{filename}'
|
|
||||||
|
|
||||||
# Check that the URL exists before returning it
|
# Check that the URL exists before returning it
|
||||||
if not check_link(url):
|
if not check_link(url):
|
||||||
raise FileNotFoundError(f'URL {url} does not exist.')
|
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
|
return url
|
||||||
|
|
||||||
@env.macro
|
@env.macro
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ nav:
|
|||||||
- Production: start/bare_prod.md
|
- Production: start/bare_prod.md
|
||||||
- Development: start/bare_dev.md
|
- Development: start/bare_dev.md
|
||||||
- Serving Files: start/serving_files.md
|
- Serving Files: start/serving_files.md
|
||||||
|
- User Accounts: start/accounts.md
|
||||||
- Data Backup: start/backup.md
|
- Data Backup: start/backup.md
|
||||||
- Migrating Data: start/migrate.md
|
- Migrating Data: start/migrate.md
|
||||||
- Advanced Topics: start/advanced.md
|
- Advanced Topics: start/advanced.md
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern": "https://www.reddit.com/r/InvenTree/"
|
"pattern": "https://www.reddit.com/r/InvenTree/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "https://opensource.org/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,11 +383,26 @@ class BulkDeleteMixin:
|
|||||||
|
|
||||||
# Filter by provided item ID values
|
# Filter by provided item ID values
|
||||||
if items:
|
if items:
|
||||||
queryset = queryset.filter(id__in=items)
|
try:
|
||||||
|
queryset = queryset.filter(id__in=items)
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError({
|
||||||
|
'non_field_errors': _('Invalid items list provided')
|
||||||
|
})
|
||||||
|
|
||||||
# Filter by provided filters
|
# Filter by provided filters
|
||||||
if filters:
|
if filters:
|
||||||
queryset = queryset.filter(**filters)
|
try:
|
||||||
|
queryset = queryset.filter(**filters)
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError({
|
||||||
|
'non_field_errors': _('Invalid filters provided')
|
||||||
|
})
|
||||||
|
|
||||||
|
if queryset.count() == 0:
|
||||||
|
raise ValidationError({
|
||||||
|
'non_field_errors': _('No items found to delete')
|
||||||
|
})
|
||||||
|
|
||||||
# Run a final validation step (should raise an error if the deletion should not proceed)
|
# Run a final validation step (should raise an error if the deletion should not proceed)
|
||||||
self.validate_delete(queryset, request)
|
self.validate_delete(queryset, request)
|
||||||
|
|||||||
@@ -40,9 +40,14 @@ class InvenTreeConfig(AppConfig):
|
|||||||
- Adding users set in the current environment
|
- Adding users set in the current environment
|
||||||
"""
|
"""
|
||||||
# skip loading if plugin registry is not loaded or we run in a background thread
|
# skip loading if plugin registry is not loaded or we run in a background thread
|
||||||
|
|
||||||
|
if not InvenTree.ready.isPluginRegistryLoaded():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip if not in worker or main thread
|
||||||
if (
|
if (
|
||||||
not InvenTree.ready.isPluginRegistryLoaded()
|
not InvenTree.ready.isInMainThread()
|
||||||
or not InvenTree.ready.isInMainThread()
|
and not InvenTree.ready.isInWorkerThread()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -52,7 +57,6 @@ class InvenTreeConfig(AppConfig):
|
|||||||
|
|
||||||
if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV:
|
if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV:
|
||||||
self.remove_obsolete_tasks()
|
self.remove_obsolete_tasks()
|
||||||
|
|
||||||
self.collect_tasks()
|
self.collect_tasks()
|
||||||
self.start_background_tasks()
|
self.start_background_tasks()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
@@ -21,13 +20,11 @@ from django.core.files.storage import Storage, default_storage
|
|||||||
from django.http import StreamingHttpResponse
|
from django.http import StreamingHttpResponse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
import bleach
|
||||||
import pytz
|
import pytz
|
||||||
import regex
|
|
||||||
from bleach import clean
|
from bleach import clean
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
import InvenTree.version
|
|
||||||
from common.currency import currency_code_default
|
from common.currency import currency_code_default
|
||||||
|
|
||||||
from .settings import MEDIA_URL, STATIC_URL
|
from .settings import MEDIA_URL, STATIC_URL
|
||||||
@@ -143,6 +140,8 @@ def getStaticUrl(filename):
|
|||||||
|
|
||||||
def TestIfImage(img):
|
def TestIfImage(img):
|
||||||
"""Test if an image file is indeed an image."""
|
"""Test if an image file is indeed an image."""
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Image.open(img).verify()
|
Image.open(img).verify()
|
||||||
return True
|
return True
|
||||||
@@ -784,32 +783,85 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
|
|||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
def remove_non_printable_characters(
|
def remove_non_printable_characters(value: str, remove_newline=True) -> str:
|
||||||
value: str, remove_newline=True, remove_ascii=True, remove_unicode=True
|
|
||||||
):
|
|
||||||
"""Remove non-printable / control characters from the provided string."""
|
"""Remove non-printable / control characters from the provided string."""
|
||||||
cleaned = value
|
cleaned = value
|
||||||
|
|
||||||
if remove_ascii:
|
# Remove ASCII control characters
|
||||||
# Remove ASCII control characters
|
# Note that we do not sub out 0x0A (\n) here, it is done separately below
|
||||||
# Note that we do not sub out 0x0A (\n) here, it is done separately below
|
regex = re.compile(r'[\u0000-\u0009\u000B-\u001F\u007F-\u009F]')
|
||||||
cleaned = regex.sub('[\x00-\x09]+', '', cleaned)
|
cleaned = regex.sub('', cleaned)
|
||||||
cleaned = regex.sub('[\x0b-\x1f\x7f]+', '', cleaned)
|
|
||||||
|
# Remove Unicode control characters
|
||||||
|
regex = re.compile(r'[\u200E\u200F\u202A-\u202E]')
|
||||||
|
cleaned = regex.sub('', cleaned)
|
||||||
|
|
||||||
if remove_newline:
|
if remove_newline:
|
||||||
cleaned = regex.sub('[\x0a]+', '', cleaned)
|
regex = re.compile(r'[\x0A]')
|
||||||
|
cleaned = regex.sub('', cleaned)
|
||||||
if remove_unicode:
|
|
||||||
# Remove Unicode control characters
|
|
||||||
if remove_newline:
|
|
||||||
cleaned = regex.sub('[^\P{C}]+', '', cleaned)
|
|
||||||
else:
|
|
||||||
# Use 'negative-lookahead' to exclude newline character
|
|
||||||
cleaned = regex.sub('(?![\x0a])[^\P{C}]+', '', cleaned)
|
|
||||||
|
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
def clean_markdown(value: str):
|
||||||
|
"""Clean a markdown string.
|
||||||
|
|
||||||
|
This function will remove javascript and other potentially harmful content from the markdown string.
|
||||||
|
"""
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
try:
|
||||||
|
markdownify_settings = settings.MARKDOWNIFY['default']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
markdownify_settings = {}
|
||||||
|
|
||||||
|
extensions = markdownify_settings.get('MARKDOWN_EXTENSIONS', [])
|
||||||
|
extension_configs = markdownify_settings.get('MARKDOWN_EXTENSION_CONFIGS', {})
|
||||||
|
|
||||||
|
# Generate raw HTML from provided markdown (without sanitizing)
|
||||||
|
# Note: The 'html' output_format is required to generate self closing tags, e.g. <tag> instead of <tag />
|
||||||
|
html = markdown.markdown(
|
||||||
|
value or '',
|
||||||
|
extensions=extensions,
|
||||||
|
extension_configs=extension_configs,
|
||||||
|
output_format='html',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bleach settings
|
||||||
|
whitelist_tags = markdownify_settings.get(
|
||||||
|
'WHITELIST_TAGS', bleach.sanitizer.ALLOWED_TAGS
|
||||||
|
)
|
||||||
|
whitelist_attrs = markdownify_settings.get(
|
||||||
|
'WHITELIST_ATTRS', bleach.sanitizer.ALLOWED_ATTRIBUTES
|
||||||
|
)
|
||||||
|
whitelist_styles = markdownify_settings.get(
|
||||||
|
'WHITELIST_STYLES', bleach.css_sanitizer.ALLOWED_CSS_PROPERTIES
|
||||||
|
)
|
||||||
|
whitelist_protocols = markdownify_settings.get(
|
||||||
|
'WHITELIST_PROTOCOLS', bleach.sanitizer.ALLOWED_PROTOCOLS
|
||||||
|
)
|
||||||
|
strip = markdownify_settings.get('STRIP', True)
|
||||||
|
|
||||||
|
css_sanitizer = bleach.css_sanitizer.CSSSanitizer(
|
||||||
|
allowed_css_properties=whitelist_styles
|
||||||
|
)
|
||||||
|
cleaner = bleach.Cleaner(
|
||||||
|
tags=whitelist_tags,
|
||||||
|
attributes=whitelist_attrs,
|
||||||
|
css_sanitizer=css_sanitizer,
|
||||||
|
protocols=whitelist_protocols,
|
||||||
|
strip=strip,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean the HTML content (for comparison). This must be the same as the original content
|
||||||
|
clean_html = cleaner.clean(html)
|
||||||
|
|
||||||
|
if html != clean_html:
|
||||||
|
raise ValidationError(_('Data contains prohibited markdown content'))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def hash_barcode(barcode_data):
|
def hash_barcode(barcode_data):
|
||||||
"""Calculate a 'unique' hash for a barcode string.
|
"""Calculate a 'unique' hash for a barcode string.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ from rest_framework import generics, mixins, status
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from InvenTree.fields import InvenTreeNotesField
|
from InvenTree.fields import InvenTreeNotesField
|
||||||
from InvenTree.helpers import remove_non_printable_characters, strip_html_tags
|
from InvenTree.helpers import (
|
||||||
|
clean_markdown,
|
||||||
|
remove_non_printable_characters,
|
||||||
|
strip_html_tags,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CleanMixin:
|
class CleanMixin:
|
||||||
@@ -57,6 +61,7 @@ class CleanMixin:
|
|||||||
|
|
||||||
# By default, newline characters are removed
|
# By default, newline characters are removed
|
||||||
remove_newline = True
|
remove_newline = True
|
||||||
|
is_markdown = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(self, 'serializer_class'):
|
if hasattr(self, 'serializer_class'):
|
||||||
@@ -64,11 +69,12 @@ class CleanMixin:
|
|||||||
field = model._meta.get_field(field)
|
field = model._meta.get_field(field)
|
||||||
|
|
||||||
# The following field types allow newline characters
|
# The following field types allow newline characters
|
||||||
allow_newline = [InvenTreeNotesField]
|
allow_newline = [(InvenTreeNotesField, True)]
|
||||||
|
|
||||||
for field_type in allow_newline:
|
for field_type in allow_newline:
|
||||||
if issubclass(type(field), field_type):
|
if issubclass(type(field), field_type[0]):
|
||||||
remove_newline = False
|
remove_newline = False
|
||||||
|
is_markdown = field_type[1]
|
||||||
break
|
break
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -80,6 +86,9 @@ class CleanMixin:
|
|||||||
cleaned, remove_newline=remove_newline
|
cleaned, remove_newline=remove_newline
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_markdown:
|
||||||
|
cleaned = clean_markdown(cleaned)
|
||||||
|
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
def clean_data(self, data: dict) -> dict:
|
def clean_data(self, data: dict) -> dict:
|
||||||
|
|||||||
@@ -882,8 +882,8 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.remote_image_file = download_image_from_url(url)
|
self.remote_image_file = download_image_from_url(url)
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
self.remote_image_file = None
|
self.remote_image_file = None
|
||||||
raise ValidationError(str(exc))
|
raise ValidationError(_('Failed to download image from remote URL'))
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ from . import config, locales
|
|||||||
|
|
||||||
checkMinPythonVersion()
|
checkMinPythonVersion()
|
||||||
|
|
||||||
INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom'
|
INVENTREE_BASE_URL = 'https://inventree.org'
|
||||||
|
INVENTREE_NEWS_URL = f'{INVENTREE_BASE_URL}/news/feed.atom'
|
||||||
|
|
||||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||||
TESTING = 'test' in sys.argv or 'TESTING' in os.environ
|
TESTING = 'test' in sys.argv or 'TESTING' in os.environ
|
||||||
@@ -85,6 +86,7 @@ ENABLE_CLASSIC_FRONTEND = get_boolean_setting(
|
|||||||
# Disable CUI parts if CUI tests are disabled
|
# Disable CUI parts if CUI tests are disabled
|
||||||
if TESTING and '--exclude-tag=cui' in sys.argv:
|
if TESTING and '--exclude-tag=cui' in sys.argv:
|
||||||
ENABLE_CLASSIC_FRONTEND = False
|
ENABLE_CLASSIC_FRONTEND = False
|
||||||
|
|
||||||
ENABLE_PLATFORM_FRONTEND = get_boolean_setting(
|
ENABLE_PLATFORM_FRONTEND = get_boolean_setting(
|
||||||
'INVENTREE_PLATFORM_FRONTEND', 'platform_frontend', True
|
'INVENTREE_PLATFORM_FRONTEND', 'platform_frontend', True
|
||||||
)
|
)
|
||||||
@@ -1060,26 +1062,40 @@ if (
|
|||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
COOKIE_MODE = (
|
COOKIE_MODE = (
|
||||||
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'None'))
|
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'False'))
|
||||||
.lower()
|
.lower()
|
||||||
.strip()
|
.strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
valid_cookie_modes = {'lax': 'Lax', 'strict': 'Strict', 'none': None, 'null': None}
|
# Valid modes (as per the django settings documentation)
|
||||||
|
valid_cookie_modes = ['lax', 'strict', 'none']
|
||||||
|
|
||||||
if COOKIE_MODE not in valid_cookie_modes.keys():
|
if not DEBUG and not TESTING and COOKIE_MODE in valid_cookie_modes:
|
||||||
logger.error('Invalid cookie samesite mode: %s', COOKIE_MODE)
|
# Set the cookie mode (in production mode only)
|
||||||
sys.exit(-1)
|
COOKIE_MODE = COOKIE_MODE.capitalize()
|
||||||
|
else:
|
||||||
COOKIE_MODE = valid_cookie_modes[COOKIE_MODE.lower()]
|
# Default to False, as per the Django settings
|
||||||
|
COOKIE_MODE = False
|
||||||
|
|
||||||
# Additional CSRF settings
|
# Additional CSRF settings
|
||||||
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
|
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
|
||||||
CSRF_COOKIE_NAME = 'csrftoken'
|
CSRF_COOKIE_NAME = 'csrftoken'
|
||||||
|
|
||||||
CSRF_COOKIE_SAMESITE = COOKIE_MODE
|
CSRF_COOKIE_SAMESITE = COOKIE_MODE
|
||||||
SESSION_COOKIE_SAMESITE = COOKIE_MODE
|
SESSION_COOKIE_SAMESITE = COOKIE_MODE
|
||||||
SESSION_COOKIE_SECURE = get_boolean_setting(
|
|
||||||
'INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', False
|
"""Set the SESSION_COOKIE_SECURE value based on the following rules:
|
||||||
|
- False if the server is running in DEBUG mode
|
||||||
|
- True if samesite cookie setting is set to 'None'
|
||||||
|
- Otherwise, use the value specified in the configuration file (or env var)
|
||||||
|
"""
|
||||||
|
SESSION_COOKIE_SECURE = (
|
||||||
|
False
|
||||||
|
if DEBUG
|
||||||
|
else (
|
||||||
|
SESSION_COOKIE_SAMESITE == 'None'
|
||||||
|
or get_boolean_setting('INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', True)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
USE_X_FORWARDED_HOST = get_boolean_setting(
|
USE_X_FORWARDED_HOST = get_boolean_setting(
|
||||||
@@ -1231,23 +1247,29 @@ MARKDOWNIFY = {
|
|||||||
'abbr',
|
'abbr',
|
||||||
'b',
|
'b',
|
||||||
'blockquote',
|
'blockquote',
|
||||||
|
'code',
|
||||||
'em',
|
'em',
|
||||||
'h1',
|
'h1',
|
||||||
'h2',
|
'h2',
|
||||||
'h3',
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'hr',
|
||||||
'i',
|
'i',
|
||||||
'img',
|
'img',
|
||||||
'li',
|
'li',
|
||||||
'ol',
|
'ol',
|
||||||
'p',
|
'p',
|
||||||
|
'pre',
|
||||||
|
's',
|
||||||
'strong',
|
'strong',
|
||||||
'ul',
|
|
||||||
'table',
|
'table',
|
||||||
'thead',
|
'thead',
|
||||||
'tbody',
|
'tbody',
|
||||||
'th',
|
'th',
|
||||||
'tr',
|
'tr',
|
||||||
'td',
|
'td',
|
||||||
|
'ul',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import logging
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from django_q.models import Success
|
from django_q.models import Success
|
||||||
from django_q.status import Stat
|
from django_q.status import Stat
|
||||||
@@ -63,13 +62,13 @@ def check_system_health(**kwargs):
|
|||||||
|
|
||||||
if not is_worker_running(**kwargs): # pragma: no cover
|
if not is_worker_running(**kwargs): # pragma: no cover
|
||||||
result = False
|
result = False
|
||||||
logger.warning(_('Background worker check failed'))
|
logger.warning('Background worker check failed')
|
||||||
|
|
||||||
if not InvenTree.email.is_email_configured(): # pragma: no cover
|
if not InvenTree.email.is_email_configured(): # pragma: no cover
|
||||||
result = False
|
result = False
|
||||||
logger.warning(_('Email backend not configured'))
|
logger.warning('Email backend not configured')
|
||||||
|
|
||||||
if not result: # pragma: no cover
|
if not result: # pragma: no cover
|
||||||
logger.warning(_('InvenTree system health checks failed'))
|
logger.warning('InvenTree system health checks failed')
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -670,3 +670,15 @@ def admin_url(user, table, pk):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def cui_enabled():
|
||||||
|
"""Return True if the CUI is enabled."""
|
||||||
|
return settings.ENABLE_CLASSIC_FRONTEND
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def pui_enabled():
|
||||||
|
"""Return True if the PUI is enabled."""
|
||||||
|
return settings.ENABLE_PLATFORM_FRONTEND
|
||||||
|
|||||||
@@ -504,14 +504,14 @@ urlpatterns.append(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send any unknown URLs to the parts page
|
# Send any unknown URLs to the index page
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
re_path(
|
re_path(
|
||||||
r'^.*$',
|
r'^.*$',
|
||||||
RedirectView.as_view(
|
RedirectView.as_view(
|
||||||
url='/index/'
|
url='/index/'
|
||||||
if settings.ENABLE_CLASSIC_FRONTEND
|
if settings.ENABLE_CLASSIC_FRONTEND
|
||||||
else settings.FRONTEND_URL_BASE,
|
else f'/{settings.FRONTEND_URL_BASE}/',
|
||||||
permanent=False,
|
permanent=False,
|
||||||
),
|
),
|
||||||
name='index',
|
name='index',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from django.conf import settings
|
|||||||
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
|
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
|
||||||
|
|
||||||
# InvenTree software version
|
# InvenTree software version
|
||||||
INVENTREE_SW_VERSION = '0.16.3'
|
INVENTREE_SW_VERSION = '0.16.9'
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|||||||
@@ -1502,12 +1502,19 @@ class BuildItem(InvenTree.models.InvenTreeMetadataModel):
|
|||||||
'quantity': _(f'Allocated quantity ({q}) must not exceed available stock quantity ({a})')
|
'quantity': _(f'Allocated quantity ({q}) must not exceed available stock quantity ({a})')
|
||||||
})
|
})
|
||||||
|
|
||||||
# Allocated quantity cannot cause the stock item to be over-allocated
|
# Ensure that we do not 'over allocate' a stock item
|
||||||
available = decimal.Decimal(self.stock_item.quantity)
|
available = decimal.Decimal(self.stock_item.quantity)
|
||||||
allocated = decimal.Decimal(self.stock_item.allocation_count())
|
|
||||||
quantity = decimal.Decimal(self.quantity)
|
quantity = decimal.Decimal(self.quantity)
|
||||||
|
build_allocation_count = decimal.Decimal(self.stock_item.build_allocation_count(
|
||||||
|
exclude_allocations={'pk': self.pk}
|
||||||
|
))
|
||||||
|
sales_allocation_count = decimal.Decimal(self.stock_item.sales_order_allocation_count())
|
||||||
|
|
||||||
if available - allocated + quantity < quantity:
|
total_allocation = (
|
||||||
|
build_allocation_count + sales_allocation_count + quantity
|
||||||
|
)
|
||||||
|
|
||||||
|
if total_allocation > available:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'quantity': _('Stock item is over-allocated')
|
'quantity': _('Stock item is over-allocated')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load generic %}
|
{% load generic %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
@@ -277,7 +278,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
$('#show-qr-code').click(function() {
|
$('#show-qr-code').click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Build Order QR Code" escape %}',
|
'{% trans "Build Order QR Code" escape %}',
|
||||||
'{{ build.barcode }}'
|
`{% clean_barcode build.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class BuildTest(BuildAPITest):
|
|||||||
"status": 50, # Item requires attention
|
"status": 50, # Item requires attention
|
||||||
},
|
},
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
max_query_count=450, # TODO: Try to optimize this
|
max_query_count=600,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
||||||
@@ -920,6 +920,65 @@ class BuildAllocationTest(BuildAPITest):
|
|||||||
expected_code=201,
|
expected_code=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class BuildItemTest(BuildAPITest):
|
||||||
|
"""Unit tests for build items.
|
||||||
|
|
||||||
|
For this test, we will be using Build ID=1;
|
||||||
|
|
||||||
|
- This points to Part 100 (see fixture data in part.yaml)
|
||||||
|
- This Part already has a BOM with 4 items (see fixture data in bom.yaml)
|
||||||
|
- There are no BomItem objects yet created for this build
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Basic operation as part of test suite setup"""
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.assignRole('build.add')
|
||||||
|
self.assignRole('build.change')
|
||||||
|
|
||||||
|
self.build = Build.objects.get(pk=1)
|
||||||
|
|
||||||
|
# Regenerate BuildLine objects
|
||||||
|
self.build.create_build_line_items()
|
||||||
|
|
||||||
|
# Record number of build items which exist at the start of each test
|
||||||
|
self.n = BuildItem.objects.count()
|
||||||
|
|
||||||
|
def test_update_overallocated(self):
|
||||||
|
"""Test update of overallocated stock items."""
|
||||||
|
|
||||||
|
si = StockItem.objects.get(pk=2)
|
||||||
|
|
||||||
|
# Find line item
|
||||||
|
line = self.build.build_lines.all().filter(bom_item__sub_part=si.part).first()
|
||||||
|
|
||||||
|
# Set initial stock item quantity
|
||||||
|
si.quantity = 100
|
||||||
|
si.save()
|
||||||
|
|
||||||
|
# Create build item
|
||||||
|
bi = BuildItem(
|
||||||
|
build_line=line,
|
||||||
|
stock_item=si,
|
||||||
|
quantity=100
|
||||||
|
)
|
||||||
|
bi.save()
|
||||||
|
|
||||||
|
# Reduce stock item quantity
|
||||||
|
si.quantity = 50
|
||||||
|
si.save()
|
||||||
|
|
||||||
|
# Reduce build item quantity
|
||||||
|
url = reverse('api-build-item-detail', kwargs={'pk': bi.pk})
|
||||||
|
|
||||||
|
self.patch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
"quantity": 50,
|
||||||
|
},
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
class BuildOverallocationTest(BuildAPITest):
|
class BuildOverallocationTest(BuildAPITest):
|
||||||
"""Unit tests for over allocation of stock items against a build order.
|
"""Unit tests for over allocation of stock items against a build order.
|
||||||
|
|||||||
@@ -29,5 +29,5 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(set_default_currency),
|
migrations.RunPython(set_default_currency, reverse_code=migrations.RunPython.noop),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData, isRebuildingData
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
from plugin.models import NotificationUserSetting, PluginConfig
|
from plugin.models import NotificationUserSetting, PluginConfig
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
@@ -185,9 +185,20 @@ class MethodStorageClass:
|
|||||||
Is initialized on startup as one instance named `storage` in this file.
|
Is initialized on startup as one instance named `storage` in this file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
liste = None
|
methods_list = None
|
||||||
user_settings = {}
|
user_settings = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def methods(self):
|
||||||
|
"""Return all available methods.
|
||||||
|
|
||||||
|
This is cached, and stored internally.
|
||||||
|
"""
|
||||||
|
if self.methods_list is None:
|
||||||
|
self.collect()
|
||||||
|
|
||||||
|
return self.methods_list
|
||||||
|
|
||||||
def collect(self, selected_classes=None):
|
def collect(self, selected_classes=None):
|
||||||
"""Collect all classes in the environment that are notification methods.
|
"""Collect all classes in the environment that are notification methods.
|
||||||
|
|
||||||
@@ -196,7 +207,8 @@ class MethodStorageClass:
|
|||||||
Args:
|
Args:
|
||||||
selected_classes (class, optional): References to the classes that should be registered. Defaults to None.
|
selected_classes (class, optional): References to the classes that should be registered. Defaults to None.
|
||||||
"""
|
"""
|
||||||
logger.debug('Collecting notification methods')
|
logger.debug('Collecting notification methods...')
|
||||||
|
|
||||||
current_method = (
|
current_method = (
|
||||||
InvenTree.helpers.inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS
|
InvenTree.helpers.inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS
|
||||||
)
|
)
|
||||||
@@ -219,8 +231,12 @@ class MethodStorageClass:
|
|||||||
item.plugin = plugin() if plugin else None
|
item.plugin = plugin() if plugin else None
|
||||||
filtered_list[ref] = item
|
filtered_list[ref] = item
|
||||||
|
|
||||||
storage.liste = list(filtered_list.values())
|
storage.methods_list = list(filtered_list.values())
|
||||||
logger.info('Found %s notification methods', len(storage.liste))
|
|
||||||
|
logger.info('Found %s notification methods', len(storage.methods_list))
|
||||||
|
|
||||||
|
for item in storage.methods_list:
|
||||||
|
logger.debug(' - %s', str(item))
|
||||||
|
|
||||||
def get_usersettings(self, user) -> list:
|
def get_usersettings(self, user) -> list:
|
||||||
"""Returns all user settings for a specific user.
|
"""Returns all user settings for a specific user.
|
||||||
@@ -234,7 +250,8 @@ class MethodStorageClass:
|
|||||||
list: All applicablae notification settings.
|
list: All applicablae notification settings.
|
||||||
"""
|
"""
|
||||||
methods = []
|
methods = []
|
||||||
for item in storage.liste:
|
|
||||||
|
for item in storage.methods:
|
||||||
if item.USER_SETTING:
|
if item.USER_SETTING:
|
||||||
new_key = f'NOTIFICATION_METHOD_{item.METHOD_NAME.upper()}'
|
new_key = f'NOTIFICATION_METHOD_{item.METHOD_NAME.upper()}'
|
||||||
|
|
||||||
@@ -250,6 +267,7 @@ class MethodStorageClass:
|
|||||||
'icon': getattr(item, 'METHOD_ICON', ''),
|
'icon': getattr(item, 'METHOD_ICON', ''),
|
||||||
'method': item.METHOD_NAME,
|
'method': item.METHOD_NAME,
|
||||||
})
|
})
|
||||||
|
|
||||||
return methods
|
return methods
|
||||||
|
|
||||||
|
|
||||||
@@ -352,7 +370,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
|||||||
delivery_methods = kwargs.get('delivery_methods', None)
|
delivery_methods = kwargs.get('delivery_methods', None)
|
||||||
|
|
||||||
# Check if data is importing currently
|
# Check if data is importing currently
|
||||||
if isImportingData():
|
if isImportingData() or isRebuildingData():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Resolve object reference
|
# Resolve object reference
|
||||||
@@ -422,7 +440,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
|||||||
|
|
||||||
# Collect possible methods
|
# Collect possible methods
|
||||||
if delivery_methods is None:
|
if delivery_methods is None:
|
||||||
delivery_methods = storage.liste or []
|
delivery_methods = storage.methods or []
|
||||||
else:
|
else:
|
||||||
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
|
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
|
||||||
|
|
||||||
@@ -439,7 +457,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
|||||||
# Set delivery flag
|
# Set delivery flag
|
||||||
common.models.NotificationEntry.notify(category, obj_ref_value)
|
common.models.NotificationEntry.notify(category, obj_ref_value)
|
||||||
else:
|
else:
|
||||||
logger.debug("No possible users for notification '%s'", category)
|
logger.info("No possible users for notification '%s'", category)
|
||||||
|
|
||||||
|
|
||||||
def trigger_superuser_notification(plugin: PluginConfig, msg: str):
|
def trigger_superuser_notification(plugin: PluginConfig, msg: str):
|
||||||
|
|||||||
@@ -546,6 +546,10 @@ class AttachmentSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
model_type = self.validated_data.get('model_type', None)
|
model_type = self.validated_data.get('model_type', None)
|
||||||
|
|
||||||
|
# If the model type is not specified, attempt to infer it from the instance
|
||||||
|
if model_type is None and self.instance:
|
||||||
|
model_type = self.instance.model_type
|
||||||
|
|
||||||
# Ensure that the user has permission to attach files to the specified model
|
# Ensure that the user has permission to attach files to the specified model
|
||||||
user = self.context.get('request').user
|
user = self.context.get('request').user
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ def update_news_feed():
|
|||||||
if entry.id in id_list:
|
if entry.id in id_list:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Enforce proper links for the entries
|
||||||
|
if entry.link and str(entry.link).startswith('/'):
|
||||||
|
entry.link = settings.INVENTREE_BASE_URL + str(entry.link)
|
||||||
|
|
||||||
# Create entry
|
# Create entry
|
||||||
try:
|
try:
|
||||||
NewsFeedEntry.objects.create(
|
NewsFeedEntry.objects.create(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends "page_base.html" %}
|
{% extends "page_base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
@@ -303,7 +304,7 @@ onPanelLoad('supplier-part-notes', function() {
|
|||||||
$("#show-qr-code").click(function() {
|
$("#show-qr-code").click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Supplier Part QR Code" escape %}',
|
'{% trans "Supplier Part QR Code" escape %}',
|
||||||
'{{ part.barcode }}'
|
`{% clean_barcode part.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,52 @@ class CompanyTest(InvenTreeAPITestCase):
|
|||||||
len(self.get(url, data={'active': False}, expected_code=200).data), 1
|
len(self.get(url, data={'active': False}, expected_code=200).data), 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_company_notes(self):
|
||||||
|
"""Test the markdown 'notes' field for the Company model."""
|
||||||
|
pk = Company.objects.first().pk
|
||||||
|
url = reverse('api-company-detail', kwargs={'pk': pk})
|
||||||
|
|
||||||
|
# Attempt to inject malicious markdown into the "notes" field
|
||||||
|
xss = [
|
||||||
|
'[Click me](javascript:alert(123))',
|
||||||
|
')',
|
||||||
|
')',
|
||||||
|
]
|
||||||
|
|
||||||
|
for note in xss:
|
||||||
|
response = self.patch(url, {'notes': note}, expected_code=400)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'Data contains prohibited markdown content', str(response.data)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests with disallowed tags
|
||||||
|
invalid_tags = [
|
||||||
|
'<iframe src="javascript:alert(123)"></iframe>',
|
||||||
|
'<canvas>A disallowed tag!</canvas>',
|
||||||
|
]
|
||||||
|
|
||||||
|
for note in invalid_tags:
|
||||||
|
response = self.patch(url, {'notes': note}, expected_code=400)
|
||||||
|
|
||||||
|
self.assertIn('Remove HTML tags from this value', str(response.data))
|
||||||
|
|
||||||
|
# The following markdown is safe, and should be accepted
|
||||||
|
good = [
|
||||||
|
'This is a **bold** statement',
|
||||||
|
'This is a *italic* statement',
|
||||||
|
'This is a [link](https://www.google.com)',
|
||||||
|
'This is an ',
|
||||||
|
'This is a `code` block',
|
||||||
|
'This text has ~~strikethrough~~ formatting',
|
||||||
|
'This text has a raw link - https://www.google.com - and should still pass the test',
|
||||||
|
]
|
||||||
|
|
||||||
|
for note in good:
|
||||||
|
response = self.patch(url, {'notes': note}, expected_code=200)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['notes'], note)
|
||||||
|
|
||||||
|
|
||||||
class ContactTest(InvenTreeAPITestCase):
|
class ContactTest(InvenTreeAPITestCase):
|
||||||
"""Tests for the Contact models."""
|
"""Tests for the Contact models."""
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ use_x_forwarded_port: false
|
|||||||
# Cookie settings
|
# Cookie settings
|
||||||
cookie:
|
cookie:
|
||||||
secure: false
|
secure: false
|
||||||
samesite: none
|
samesite: false
|
||||||
|
|
||||||
# Cross Origin Resource Sharing (CORS) settings (see https://github.com/adamchainz/django-cors-headers)
|
# Cross Origin Resource Sharing (CORS) settings (see https://github.com/adamchainz/django-cors-headers)
|
||||||
cors:
|
cors:
|
||||||
|
|||||||
@@ -1098,6 +1098,8 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
self.status = SalesOrderStatus.COMPLETE.value
|
self.status = SalesOrderStatus.COMPLETE.value
|
||||||
else:
|
else:
|
||||||
self.status = SalesOrderStatus.SHIPPED.value
|
self.status = SalesOrderStatus.SHIPPED.value
|
||||||
|
|
||||||
|
if self.shipment_date is None:
|
||||||
self.shipped_by = user
|
self.shipped_by = user
|
||||||
self.shipment_date = InvenTree.helpers.current_date()
|
self.shipment_date = InvenTree.helpers.current_date()
|
||||||
|
|
||||||
@@ -2196,7 +2198,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def receive_line_item(self, line, location, user, note=''):
|
def receive_line_item(self, line, location, user, note='', **kwargs):
|
||||||
"""Receive a line item against this ReturnOrder.
|
"""Receive a line item against this ReturnOrder.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
@@ -2222,7 +2224,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
deltas['customer'] = stock_item.customer.pk
|
deltas['customer'] = stock_item.customer.pk
|
||||||
|
|
||||||
# Update the StockItem
|
# Update the StockItem
|
||||||
stock_item.status = StockStatus.QUARANTINED.value
|
stock_item.status = kwargs.get('status', StockStatus.QUARANTINED.value)
|
||||||
stock_item.location = location
|
stock_item.location = location
|
||||||
stock_item.customer = None
|
stock_item.customer = None
|
||||||
stock_item.sales_order = None
|
stock_item.sales_order = None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load generic %}
|
{% load generic %}
|
||||||
|
|
||||||
@@ -312,7 +313,7 @@ $("#export-order").click(function() {
|
|||||||
$('#show-qr-code').click(function() {
|
$('#show-qr-code').click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Purchase Order QR Code" escape %}',
|
'{% trans "Purchase Order QR Code" escape %}',
|
||||||
'{{ order.barcode }}'
|
`{% clean_barcode order.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load generic %}
|
{% load generic %}
|
||||||
|
|
||||||
@@ -257,7 +258,7 @@ $('#print-order-report').click(function() {
|
|||||||
$('#show-qr-code').click(function() {
|
$('#show-qr-code').click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Return Order QR Code" escape %}',
|
'{% trans "Return Order QR Code" escape %}',
|
||||||
'{{ order.barcode }}'
|
`{% clean_barcode order.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load generic %}
|
{% load generic %}
|
||||||
|
|
||||||
@@ -319,7 +320,7 @@ $('#print-order-report').click(function() {
|
|||||||
$('#show-qr-code').click(function() {
|
$('#show-qr-code').click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Sales Order QR Code" escape %}',
|
'{% trans "Sales Order QR Code" escape %}',
|
||||||
'{{ order.barcode }}'
|
`{% clean_barcode order.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1576,6 +1576,8 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
so.refresh_from_db()
|
so.refresh_from_db()
|
||||||
self.assertEqual(so.status, SalesOrderStatus.SHIPPED.value)
|
self.assertEqual(so.status, SalesOrderStatus.SHIPPED.value)
|
||||||
|
self.assertIsNotNone(so.shipment_date)
|
||||||
|
self.assertIsNotNone(so.shipped_by)
|
||||||
|
|
||||||
# Now, let's try to "complete" the shipment again
|
# Now, let's try to "complete" the shipment again
|
||||||
# This time it should get marked as "COMPLETE"
|
# This time it should get marked as "COMPLETE"
|
||||||
@@ -1591,9 +1593,14 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
# Next, we'll change the setting so that the order status jumps straight to "complete"
|
# Next, we'll change the setting so that the order status jumps straight to "complete"
|
||||||
so.status = SalesOrderStatus.PENDING.value
|
so.status = SalesOrderStatus.PENDING.value
|
||||||
|
so.shipment_date = None
|
||||||
|
so.shipped_by = None
|
||||||
so.save()
|
so.save()
|
||||||
so.refresh_from_db()
|
so.refresh_from_db()
|
||||||
|
|
||||||
self.assertEqual(so.status, SalesOrderStatus.PENDING.value)
|
self.assertEqual(so.status, SalesOrderStatus.PENDING.value)
|
||||||
|
self.assertIsNone(so.shipped_by)
|
||||||
|
self.assertIsNone(so.shipment_date)
|
||||||
|
|
||||||
InvenTreeSetting.set_setting('SALESORDER_SHIP_COMPLETE', True)
|
InvenTreeSetting.set_setting('SALESORDER_SHIP_COMPLETE', True)
|
||||||
|
|
||||||
@@ -1603,6 +1610,9 @@ class SalesOrderTest(OrderTest):
|
|||||||
so.refresh_from_db()
|
so.refresh_from_db()
|
||||||
self.assertEqual(so.status, SalesOrderStatus.COMPLETE.value)
|
self.assertEqual(so.status, SalesOrderStatus.COMPLETE.value)
|
||||||
|
|
||||||
|
self.assertIsNotNone(so.shipment_date)
|
||||||
|
self.assertIsNotNone(so.shipped_by)
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderLineItemTest(OrderTest):
|
class SalesOrderLineItemTest(OrderTest):
|
||||||
"""Tests for the SalesOrderLineItem API."""
|
"""Tests for the SalesOrderLineItem API."""
|
||||||
|
|||||||
@@ -353,7 +353,8 @@ class LineItemPricing(PartPricing):
|
|||||||
try:
|
try:
|
||||||
part_id = self.request.POST.get('pk')
|
part_id = self.request.POST.get('pk')
|
||||||
part = Part.objects.get(id=part_id)
|
part = Part.objects.get(id=part_id)
|
||||||
except Part.DoesNotExist:
|
except Exception:
|
||||||
|
# Part not found, or invalid ID
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
@@ -451,7 +452,7 @@
|
|||||||
$("#show-qr-code").click(function() {
|
$("#show-qr-code").click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Part QR Code" escape %}',
|
'{% trans "Part QR Code" escape %}',
|
||||||
'{{ part.barcode }}',
|
`{% clean_barcode part.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ def notification_list(context, *args, **kwargs):
|
|||||||
'description': a.__doc__,
|
'description': a.__doc__,
|
||||||
'name': a.__name__,
|
'name': a.__name__,
|
||||||
}
|
}
|
||||||
for a in storage.liste
|
for a in storage.methods
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,12 +125,14 @@ class ReportConfig(AppConfig):
|
|||||||
# Read the existing template file
|
# Read the existing template file
|
||||||
data = template_file.open('r').read()
|
data = template_file.open('r').read()
|
||||||
|
|
||||||
logger.info("Creating new label template: '%s'", template['name'])
|
try:
|
||||||
|
# Create a new entry
|
||||||
# Create a new entry
|
report.models.LabelTemplate.objects.create(
|
||||||
report.models.LabelTemplate.objects.create(
|
**template, template=ContentFile(data, os.path.basename(filename))
|
||||||
**template, template=ContentFile(data, os.path.basename(filename))
|
)
|
||||||
)
|
logger.info("Creating new label template: '%s'", template['name'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def create_default_reports(self):
|
def create_default_reports(self):
|
||||||
"""Create default report templates."""
|
"""Create default report templates."""
|
||||||
@@ -212,9 +214,11 @@ class ReportConfig(AppConfig):
|
|||||||
# Read the existing template file
|
# Read the existing template file
|
||||||
data = template_file.open('r').read()
|
data = template_file.open('r').read()
|
||||||
|
|
||||||
logger.info("Creating new report template: '%s'", template['name'])
|
|
||||||
|
|
||||||
# Create a new entry
|
# Create a new entry
|
||||||
report.models.ReportTemplate.objects.create(
|
try:
|
||||||
**template, template=ContentFile(data, os.path.basename(filename))
|
report.models.ReportTemplate.objects.create(
|
||||||
)
|
**template, template=ContentFile(data, os.path.basename(filename))
|
||||||
|
)
|
||||||
|
logger.info("Created new report template: '%s'", template['name'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Template tags for rendering various barcodes."""
|
"""Template tags for rendering various barcodes."""
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
import barcode as python_barcode
|
import barcode as python_barcode
|
||||||
import qrcode.constants as ECL
|
import qrcode.constants as ECL
|
||||||
@@ -26,6 +27,23 @@ def image_data(img, fmt='PNG'):
|
|||||||
return report.helpers.encode_image_base64(img, fmt)
|
return report.helpers.encode_image_base64(img, fmt)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def clean_barcode(data):
|
||||||
|
"""Return a 'cleaned' string for encoding into a barcode / qrcode.
|
||||||
|
|
||||||
|
- This function runs the data through bleach, and removes any malicious HTML content.
|
||||||
|
- Used to render raw barcode data into the rendered HTML templates
|
||||||
|
"""
|
||||||
|
from InvenTree.helpers import strip_html_tags
|
||||||
|
|
||||||
|
cleaned_date = strip_html_tags(data)
|
||||||
|
|
||||||
|
# Remove back-tick character (prevent injection)
|
||||||
|
cleaned_date = cleaned_date.replace('`', '')
|
||||||
|
|
||||||
|
return mark_safe(cleaned_date)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def qrcode(data, **kwargs):
|
def qrcode(data, **kwargs):
|
||||||
"""Return a byte-encoded QR code image.
|
"""Return a byte-encoded QR code image.
|
||||||
|
|||||||
@@ -1161,10 +1161,12 @@ class StockItem(
|
|||||||
location=location,
|
location=location,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Clear out allocation information for the stock item
|
||||||
self.customer = None
|
self.customer = None
|
||||||
self.belongs_to = None
|
self.belongs_to = None
|
||||||
self.sales_order = None
|
self.sales_order = None
|
||||||
self.location = location
|
self.location = location
|
||||||
|
self.clearAllocations()
|
||||||
|
|
||||||
trigger_event('stockitem.returnedfromcustomer', id=self.id)
|
trigger_event('stockitem.returnedfromcustomer', id=self.id)
|
||||||
|
|
||||||
@@ -1189,9 +1191,17 @@ class StockItem(
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def build_allocation_count(self):
|
def build_allocation_count(self, **kwargs):
|
||||||
"""Return the total quantity allocated to builds."""
|
"""Return the total quantity allocated to builds, with optional filters."""
|
||||||
query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
|
query = self.allocations.all()
|
||||||
|
|
||||||
|
if filter_allocations := kwargs.get('filter_allocations'):
|
||||||
|
query = query.filter(**filter_allocations)
|
||||||
|
|
||||||
|
if exclude_allocations := kwargs.get('exclude_allocations'):
|
||||||
|
query = query.exclude(**exclude_allocations)
|
||||||
|
|
||||||
|
query = query.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
|
||||||
|
|
||||||
total = query['q']
|
total = query['q']
|
||||||
|
|
||||||
@@ -1577,6 +1587,13 @@ class StockItem(
|
|||||||
# Remove the equivalent number of items
|
# Remove the equivalent number of items
|
||||||
self.take_stock(quantity, user, notes=notes)
|
self.take_stock(quantity, user, notes=notes)
|
||||||
|
|
||||||
|
# Rebuild the stock tree
|
||||||
|
try:
|
||||||
|
StockItem.objects.partial_rebuild(tree_id=self.tree_id)
|
||||||
|
except Exception:
|
||||||
|
logger.warning('Failed to rebuild stock tree during serializeStock')
|
||||||
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def copyHistoryFrom(self, other):
|
def copyHistoryFrom(self, other):
|
||||||
"""Copy stock history from another StockItem."""
|
"""Copy stock history from another StockItem."""
|
||||||
@@ -1761,7 +1778,7 @@ class StockItem(
|
|||||||
|
|
||||||
# Any "sales order allocations" for the other item must be assigned to this one
|
# Any "sales order allocations" for the other item must be assigned to this one
|
||||||
for allocation in other.sales_order_allocations.all():
|
for allocation in other.sales_order_allocations.all():
|
||||||
allocation.stock_item = self()
|
allocation.stock_item = self
|
||||||
allocation.save()
|
allocation.save()
|
||||||
|
|
||||||
# Prevent atomicity issues when we are merging our own "parent" part in
|
# Prevent atomicity issues when we are merging our own "parent" part in
|
||||||
@@ -1811,7 +1828,7 @@ class StockItem(
|
|||||||
for tree_id in tree_ids:
|
for tree_id in tree_ids:
|
||||||
StockItem.objects.partial_rebuild(tree_id=tree_id)
|
StockItem.objects.partial_rebuild(tree_id=tree_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning('Rebuilding entire StockItem tree')
|
logger.warning('Rebuilding entire StockItem tree during merge_stock_items')
|
||||||
StockItem.objects.rebuild()
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@@ -2304,7 +2321,7 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
|
|||||||
"""Hook function to be executed after StockItem object is saved/updated."""
|
"""Hook function to be executed after StockItem object is saved/updated."""
|
||||||
from part import tasks as part_tasks
|
from part import tasks as part_tasks
|
||||||
|
|
||||||
if created and not InvenTree.ready.isImportingData():
|
if not InvenTree.ready.isImportingData():
|
||||||
if InvenTree.ready.canAppAccessDatabase(allow_test=True):
|
if InvenTree.ready.canAppAccessDatabase(allow_test=True):
|
||||||
InvenTree.tasks.offload_task(
|
InvenTree.tasks.offload_task(
|
||||||
part_tasks.notify_low_stock_if_required, instance.part
|
part_tasks.notify_low_stock_if_required, instance.part
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load plugin_extras %}
|
{% load plugin_extras %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load generic %}
|
{% load generic %}
|
||||||
|
{% load barcode %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load l10n %}
|
{% load l10n %}
|
||||||
|
|
||||||
@@ -534,7 +535,7 @@ $('#stock-edit-status').click(function () {
|
|||||||
$("#show-qr-code").click(function() {
|
$("#show-qr-code").click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Stock Item QR Code" escape %}',
|
'{% trans "Stock Item QR Code" escape %}',
|
||||||
'{{ item.barcode }}',
|
`{% clean_barcode item.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends "stock/stock_app_base.html" %}
|
{% extends "stock/stock_app_base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load plugin_extras %}
|
{% load plugin_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
@@ -391,7 +392,7 @@
|
|||||||
$('#show-qr-code').click(function() {
|
$('#show-qr-code').click(function() {
|
||||||
showQRDialog(
|
showQRDialog(
|
||||||
'{% trans "Stock Location QR Code" escape %}',
|
'{% trans "Stock Location QR Code" escape %}',
|
||||||
'{{ location.barcode }}'
|
`{% clean_barcode location.barcode %}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1713,7 +1713,7 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
# Now, let's delete all the newly created items with a single API request
|
# Now, let's delete all the newly created items with a single API request
|
||||||
# However, we will provide incorrect filters
|
# However, we will provide incorrect filters
|
||||||
response = self.delete(
|
response = self.delete(
|
||||||
url, {'items': tests, 'filters': {'stock_item': 10}}, expected_code=204
|
url, {'items': tests, 'filters': {'stock_item': 10}}, expected_code=400
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(StockItemTestResult.objects.count(), n + 50)
|
self.assertEqual(StockItemTestResult.objects.count(), n + 50)
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ function supplierPartFields(options={}) {
|
|||||||
icon: 'fa-box',
|
icon: 'fa-box',
|
||||||
},
|
},
|
||||||
pack_quantity: {},
|
pack_quantity: {},
|
||||||
|
active: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.part) {
|
if (options.part) {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% inventree_customize 'hide_pui_banner' as hidden %}
|
{% inventree_customize 'hide_pui_banner' as hidden %}
|
||||||
|
{% pui_enabled as pui %}
|
||||||
|
|
||||||
{% if not hidden %}
|
{% if pui and not hidden %}
|
||||||
<div class='alert alert-block alert-warning'>
|
<div class='alert alert-block alert-warning'>
|
||||||
{% if mode == 'admin' %}
|
{% if mode == 'admin' %}
|
||||||
{% trans "Platform UI - the new UI for InvenTree - provides more modern administration options." %}
|
{% trans "Platform UI - the new UI for InvenTree - provides more modern administration options." %}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ python-dotenv # Environment variable management
|
|||||||
pyyaml>=6.0.1 # YAML parsing
|
pyyaml>=6.0.1 # YAML parsing
|
||||||
qrcode[pil] # QR code generator
|
qrcode[pil] # QR code generator
|
||||||
rapidfuzz # Fuzzy string matching
|
rapidfuzz # Fuzzy string matching
|
||||||
regex # Advanced regular expressions
|
|
||||||
sentry-sdk # Error reporting (optional)
|
sentry-sdk # Error reporting (optional)
|
||||||
setuptools # Standard dependency
|
setuptools # Standard dependency
|
||||||
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
|
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ asgiref==3.8.1 \
|
|||||||
# via
|
# via
|
||||||
# django
|
# django
|
||||||
# django-cors-headers
|
# django-cors-headers
|
||||||
async-timeout==4.0.3 \
|
async-timeout==5.0.1 \
|
||||||
--hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \
|
--hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
|
||||||
--hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028
|
--hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
|
||||||
# via redis
|
# via redis
|
||||||
attrs==23.2.0 \
|
attrs==23.2.0 \
|
||||||
--hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \
|
--hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \
|
||||||
@@ -1353,87 +1353,6 @@ referencing==0.35.1 \
|
|||||||
# via
|
# via
|
||||||
# jsonschema
|
# jsonschema
|
||||||
# jsonschema-specifications
|
# jsonschema-specifications
|
||||||
regex==2024.4.28 \
|
|
||||||
--hash=sha256:05d9b6578a22db7dedb4df81451f360395828b04f4513980b6bd7a1412c679cc \
|
|
||||||
--hash=sha256:08a1749f04fee2811c7617fdd46d2e46d09106fa8f475c884b65c01326eb15c5 \
|
|
||||||
--hash=sha256:0940038bec2fe9e26b203d636c44d31dd8766abc1fe66262da6484bd82461ccf \
|
|
||||||
--hash=sha256:0a2a512d623f1f2d01d881513af9fc6a7c46e5cfffb7dc50c38ce959f9246c94 \
|
|
||||||
--hash=sha256:0a54a047b607fd2d2d52a05e6ad294602f1e0dec2291152b745870afc47c1397 \
|
|
||||||
--hash=sha256:0dd3f69098511e71880fb00f5815db9ed0ef62c05775395968299cb400aeab82 \
|
|
||||||
--hash=sha256:1031a5e7b048ee371ab3653aad3030ecfad6ee9ecdc85f0242c57751a05b0ac4 \
|
|
||||||
--hash=sha256:108e2dcf0b53a7c4ab8986842a8edcb8ab2e59919a74ff51c296772e8e74d0ae \
|
|
||||||
--hash=sha256:144a1fc54765f5c5c36d6d4b073299832aa1ec6a746a6452c3ee7b46b3d3b11d \
|
|
||||||
--hash=sha256:19d6c11bf35a6ad077eb23852827f91c804eeb71ecb85db4ee1386825b9dc4db \
|
|
||||||
--hash=sha256:1f687a28640f763f23f8a9801fe9e1b37338bb1ca5d564ddd41619458f1f22d1 \
|
|
||||||
--hash=sha256:224803b74aab56aa7be313f92a8d9911dcade37e5f167db62a738d0c85fdac4b \
|
|
||||||
--hash=sha256:23a412b7b1a7063f81a742463f38821097b6a37ce1e5b89dd8e871d14dbfd86b \
|
|
||||||
--hash=sha256:25f87ae6b96374db20f180eab083aafe419b194e96e4f282c40191e71980c666 \
|
|
||||||
--hash=sha256:2630ca4e152c221072fd4a56d4622b5ada876f668ecd24d5ab62544ae6793ed6 \
|
|
||||||
--hash=sha256:28e1f28d07220c0f3da0e8fcd5a115bbb53f8b55cecf9bec0c946eb9a059a94c \
|
|
||||||
--hash=sha256:2b51739ddfd013c6f657b55a508de8b9ea78b56d22b236052c3a85a675102dc6 \
|
|
||||||
--hash=sha256:2cc1b87bba1dd1a898e664a31012725e48af826bf3971e786c53e32e02adae6c \
|
|
||||||
--hash=sha256:2fef0b38c34ae675fcbb1b5db760d40c3fc3612cfa186e9e50df5782cac02bcd \
|
|
||||||
--hash=sha256:36f392dc7763fe7924575475736bddf9ab9f7a66b920932d0ea50c2ded2f5636 \
|
|
||||||
--hash=sha256:374f690e1dd0dbdcddea4a5c9bdd97632cf656c69113f7cd6a361f2a67221cb6 \
|
|
||||||
--hash=sha256:3986217ec830c2109875be740531feb8ddafe0dfa49767cdcd072ed7e8927962 \
|
|
||||||
--hash=sha256:39fb166d2196413bead229cd64a2ffd6ec78ebab83fff7d2701103cf9f4dfd26 \
|
|
||||||
--hash=sha256:4290035b169578ffbbfa50d904d26bec16a94526071ebec3dadbebf67a26b25e \
|
|
||||||
--hash=sha256:43548ad74ea50456e1c68d3c67fff3de64c6edb85bcd511d1136f9b5376fc9d1 \
|
|
||||||
--hash=sha256:44a22ae1cfd82e4ffa2066eb3390777dc79468f866f0625261a93e44cdf6482b \
|
|
||||||
--hash=sha256:457c2cd5a646dd4ed536c92b535d73548fb8e216ebee602aa9f48e068fc393f3 \
|
|
||||||
--hash=sha256:459226445c7d7454981c4c0ce0ad1a72e1e751c3e417f305722bbcee6697e06a \
|
|
||||||
--hash=sha256:47af45b6153522733aa6e92543938e97a70ce0900649ba626cf5aad290b737b6 \
|
|
||||||
--hash=sha256:499334ad139557de97cbc4347ee921c0e2b5e9c0f009859e74f3f77918339257 \
|
|
||||||
--hash=sha256:57ba112e5530530fd175ed550373eb263db4ca98b5f00694d73b18b9a02e7185 \
|
|
||||||
--hash=sha256:5ce479ecc068bc2a74cb98dd8dba99e070d1b2f4a8371a7dfe631f85db70fe6e \
|
|
||||||
--hash=sha256:5dbc1bcc7413eebe5f18196e22804a3be1bfdfc7e2afd415e12c068624d48247 \
|
|
||||||
--hash=sha256:6277d426e2f31bdbacb377d17a7475e32b2d7d1f02faaecc48d8e370c6a3ff31 \
|
|
||||||
--hash=sha256:66372c2a01782c5fe8e04bff4a2a0121a9897e19223d9eab30c54c50b2ebeb7f \
|
|
||||||
--hash=sha256:670fa596984b08a4a769491cbdf22350431970d0112e03d7e4eeaecaafcd0fec \
|
|
||||||
--hash=sha256:6f435946b7bf7a1b438b4e6b149b947c837cb23c704e780c19ba3e6855dbbdd3 \
|
|
||||||
--hash=sha256:7413167c507a768eafb5424413c5b2f515c606be5bb4ef8c5dee43925aa5718b \
|
|
||||||
--hash=sha256:7c3d389e8d76a49923683123730c33e9553063d9041658f23897f0b396b2386f \
|
|
||||||
--hash=sha256:7d77b6f63f806578c604dca209280e4c54f0fa9a8128bb8d2cc5fb6f99da4150 \
|
|
||||||
--hash=sha256:7e76b9cfbf5ced1aca15a0e5b6f229344d9b3123439ffce552b11faab0114a02 \
|
|
||||||
--hash=sha256:7f3502f03b4da52bbe8ba962621daa846f38489cae5c4a7b5d738f15f6443d17 \
|
|
||||||
--hash=sha256:7fe9739a686dc44733d52d6e4f7b9c77b285e49edf8570754b322bca6b85b4cc \
|
|
||||||
--hash=sha256:83ab366777ea45d58f72593adf35d36ca911ea8bd838483c1823b883a121b0e4 \
|
|
||||||
--hash=sha256:84077821c85f222362b72fdc44f7a3a13587a013a45cf14534df1cbbdc9a6796 \
|
|
||||||
--hash=sha256:8bb381f777351bd534462f63e1c6afb10a7caa9fa2a421ae22c26e796fe31b1f \
|
|
||||||
--hash=sha256:92da587eee39a52c91aebea8b850e4e4f095fe5928d415cb7ed656b3460ae79a \
|
|
||||||
--hash=sha256:9301cc6db4d83d2c0719f7fcda37229691745168bf6ae849bea2e85fc769175d \
|
|
||||||
--hash=sha256:965fd0cf4694d76f6564896b422724ec7b959ef927a7cb187fc6b3f4e4f59833 \
|
|
||||||
--hash=sha256:99d6a550425cc51c656331af0e2b1651e90eaaa23fb4acde577cf15068e2e20f \
|
|
||||||
--hash=sha256:99ef6289b62042500d581170d06e17f5353b111a15aa6b25b05b91c6886df8fc \
|
|
||||||
--hash=sha256:a1409c4eccb6981c7baabc8888d3550df518add6e06fe74fa1d9312c1838652d \
|
|
||||||
--hash=sha256:a74fcf77d979364f9b69fcf8200849ca29a374973dc193a7317698aa37d8b01c \
|
|
||||||
--hash=sha256:aaa179975a64790c1f2701ac562b5eeb733946eeb036b5bcca05c8d928a62f10 \
|
|
||||||
--hash=sha256:ac69b394764bb857429b031d29d9604842bc4cbfd964d764b1af1868eeebc4f0 \
|
|
||||||
--hash=sha256:b45d4503de8f4f3dc02f1d28a9b039e5504a02cc18906cfe744c11def942e9eb \
|
|
||||||
--hash=sha256:b7d893c8cf0e2429b823ef1a1d360a25950ed11f0e2a9df2b5198821832e1947 \
|
|
||||||
--hash=sha256:b8eb28995771c087a73338f695a08c9abfdf723d185e57b97f6175c5051ff1ae \
|
|
||||||
--hash=sha256:b91d529b47798c016d4b4c1d06cc826ac40d196da54f0de3c519f5a297c5076a \
|
|
||||||
--hash=sha256:bc365ce25f6c7c5ed70e4bc674f9137f52b7dd6a125037f9132a7be52b8a252f \
|
|
||||||
--hash=sha256:bf29304a8011feb58913c382902fde3395957a47645bf848eea695839aa101b7 \
|
|
||||||
--hash=sha256:c06bf3f38f0707592898428636cbb75d0a846651b053a1cf748763e3063a6925 \
|
|
||||||
--hash=sha256:c77d10ec3c1cf328b2f501ca32583625987ea0f23a0c2a49b37a39ee5c4c4630 \
|
|
||||||
--hash=sha256:cd196d056b40af073d95a2879678585f0b74ad35190fac04ca67954c582c6b61 \
|
|
||||||
--hash=sha256:d7a353ebfa7154c871a35caca7bfd8f9e18666829a1dc187115b80e35a29393e \
|
|
||||||
--hash=sha256:d84308f097d7a513359757c69707ad339da799e53b7393819ec2ea36bc4beb58 \
|
|
||||||
--hash=sha256:dd7ef715ccb8040954d44cfeff17e6b8e9f79c8019daae2fd30a8806ef5435c0 \
|
|
||||||
--hash=sha256:e672cf9caaf669053121f1766d659a8813bd547edef6e009205378faf45c67b8 \
|
|
||||||
--hash=sha256:ecc6148228c9ae25ce403eade13a0961de1cb016bdb35c6eafd8e7b87ad028b1 \
|
|
||||||
--hash=sha256:f1c5742c31ba7d72f2dedf7968998730664b45e38827637e0f04a2ac7de2f5f1 \
|
|
||||||
--hash=sha256:f1d6e4b7b2ae3a6a9df53efbf199e4bfcff0959dbdb5fd9ced34d4407348e39a \
|
|
||||||
--hash=sha256:f2fc053228a6bd3a17a9b0a3f15c3ab3cf95727b00557e92e1cfe094b88cc662 \
|
|
||||||
--hash=sha256:f57515750d07e14743db55d59759893fdb21d2668f39e549a7d6cad5d70f9fea \
|
|
||||||
--hash=sha256:f85151ec5a232335f1be022b09fbbe459042ea1951d8a48fef251223fc67eee1 \
|
|
||||||
--hash=sha256:fb0315a2b26fde4005a7c401707c5352df274460f2f85b209cf6024271373013 \
|
|
||||||
--hash=sha256:fc0916c4295c64d6890a46e02d4482bb5ccf33bf1a824c0eaa9e83b148291f90 \
|
|
||||||
--hash=sha256:fd24fd140b69f0b0bcc9165c397e9b2e89ecbeda83303abf2a072609f60239e2 \
|
|
||||||
--hash=sha256:fdae0120cddc839eb8e3c15faa8ad541cc6d906d3eb24d82fb041cfe2807bc1e \
|
|
||||||
--hash=sha256:fe00f4fe11c8a521b173e6324d862ee7ee3412bf7107570c9b564fe1119b56fb
|
|
||||||
# via -r src/backend/requirements.in
|
|
||||||
requests==2.32.3 \
|
requests==2.32.3 \
|
||||||
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
|
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
|
||||||
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
|
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function PrintingActions({
|
|||||||
url: apiUrl(ApiEndpoints.label_print),
|
url: apiUrl(ApiEndpoints.label_print),
|
||||||
title: t`Print Label`,
|
title: t`Print Label`,
|
||||||
fields: labelFields,
|
fields: labelFields,
|
||||||
timeout: (items.length + 1) * 1000,
|
timeout: (items.length + 1) * 5000,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
setPluginKey('');
|
setPluginKey('');
|
||||||
},
|
},
|
||||||
@@ -121,7 +121,7 @@ export function PrintingActions({
|
|||||||
const reportModal = useCreateApiFormModal({
|
const reportModal = useCreateApiFormModal({
|
||||||
title: t`Print Report`,
|
title: t`Print Report`,
|
||||||
url: apiUrl(ApiEndpoints.report_print),
|
url: apiUrl(ApiEndpoints.report_print),
|
||||||
timeout: (items.length + 1) * 1000,
|
timeout: (items.length + 1) * 5000,
|
||||||
fields: {
|
fields: {
|
||||||
template: {
|
template: {
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
20
tasks.py
20
tasks.py
@@ -411,6 +411,11 @@ def backup(c, clean=False, path=None):
|
|||||||
cmd = '--noinput --compress -v 2'
|
cmd = '--noinput --compress -v 2'
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
|
# Resolve the provided path
|
||||||
|
path = Path(path)
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
path = localDir().joinpath(path).resolve()
|
||||||
|
|
||||||
cmd += f' -O {path}'
|
cmd += f' -O {path}'
|
||||||
|
|
||||||
if clean:
|
if clean:
|
||||||
@@ -442,6 +447,11 @@ def restore(
|
|||||||
base_cmd = '--noinput --uncompress -v 2'
|
base_cmd = '--noinput --uncompress -v 2'
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
|
# Resolve the provided path
|
||||||
|
path = Path(path)
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
path = localDir().joinpath(path).resolve()
|
||||||
|
|
||||||
base_cmd += f' -I {path}'
|
base_cmd += f' -I {path}'
|
||||||
|
|
||||||
if ignore_database:
|
if ignore_database:
|
||||||
@@ -1418,11 +1428,11 @@ def docs_server(c, address='localhost:8080', compile_schema=False):
|
|||||||
def clear_generated(c):
|
def clear_generated(c):
|
||||||
"""Clear generated files from `inv update`."""
|
"""Clear generated files from `inv update`."""
|
||||||
# pyc/pyo files
|
# pyc/pyo files
|
||||||
run(c, 'find . -name "*.pyc" -exec rm -f {} +')
|
run(c, 'find src -name "*.pyc" -exec rm -f {} +')
|
||||||
run(c, 'find . -name "*.pyo" -exec rm -f {} +')
|
run(c, 'find src -name "*.pyo" -exec rm -f {} +')
|
||||||
# cache folders
|
# cache folders
|
||||||
run(c, 'find . -name "__pycache__" -exec rm -rf {} +')
|
run(c, 'find src -name "__pycache__" -exec rm -rf {} +')
|
||||||
|
|
||||||
# Generated translations
|
# Generated translations
|
||||||
run(c, 'find . -name "django.mo" -exec rm -f {} +')
|
run(c, 'find src -name "django.mo" -exec rm -f {} +')
|
||||||
run(c, 'find . -name "messages.mo" -exec rm -f {} +')
|
run(c, 'find src -name "messages.mo" -exec rm -f {} +')
|
||||||
|
|||||||
Reference in New Issue
Block a user