Compare commits

..

41 Commits

Author SHA1 Message Date
Oliver
e0756a7006 Fix requirements.txt (#8598)
* Fix requirements.txt

* Fix

* Adjust unit test

* Add link to ignore
2024-11-30 15:18:25 +11:00
Oliver Walters
14f35fef2b Update requirements files 2024-11-30 00:22:36 +00:00
Oliver Walters
44c1046a43 Remove regex import from helpers.py
Ref: https://github.com/inventree/InvenTree/pull/8547
2024-11-30 00:22:17 +00:00
github-actions[bot]
d0b87a1a12 Bug fix: record shipment date (#8580) (#8581)
* Bug fix: record shipment date

- Ref: https://github.com/inventree/InvenTree/pull/6449

* Update unit test

(cherry picked from commit db128f9322)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-11-28 16:00:00 +11:00
github-actions[bot]
a4894d9f4a after_save_stock_item (#8546) (#8548)
This pull request addresses the issue discussed in #8452

(cherry picked from commit ee9980e481)

Co-authored-by: Volker <skydiablo@gmx.net>
2024-11-25 22:04:12 +11:00
github-actions[bot]
884851733a Add info on --skip-backup (#8541) (#8542)
(cherry picked from commit f6124fc4a1)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-11-24 15:52:25 +11:00
github-actions[bot]
847322e474 UI link fix (#8508) (#8509)
* Add tags to test if CUI or PUI are enabled

* Fix PUI redirect

(cherry picked from commit 6bf6733268)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-11-18 11:39:10 +11:00
Oliver
e56d013e2e Update version.py
Bump version number to 0.16.9
2024-11-16 16:18:00 +11:00
github-actions[bot]
8c13ccf59e Env var docs (#8499) (#8500)
- Add example for specifying a list value
- Closes https://github.com/inventree/InvenTree/issues/8410

(cherry picked from commit 88bb06e75a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-11-16 16:17:08 +11:00
github-actions[bot]
dca5fd69ec Update faq.md (#8489) (#8490)
(cherry picked from commit ac63b10197)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-11-15 22:56:19 +11:00
github-actions[bot]
dce95d824a Improve handling for raw github repo links (#8483) (#8484)
* Improve handling for raw github repo links

* Fix strict mode

(cherry picked from commit b1a5b1a3bb)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-11-14 16:10:27 +11:00
Oliver
f23d405392 Update version.py (#8443)
Bump version number to 0.16.8
2024-11-07 13:29:37 +11:00
Oliver
3fe04747d7 narrw scope of cleanup (#8441) (#8442)
Co-authored-by: Matthias Mair <code@mjmair.com>
2024-11-07 13:29:28 +11:00
github-actions[bot]
8ff4eddeb9 Update docker_install.md (#8387) (#8388)
(cherry picked from commit 178f939e42)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-29 11:37:05 +11:00
Oliver
de2edc4ed6 Samesite fix (#8390)
* Fix for migratoin

* Fix for COOKIE_MODE

- Update to match master

* Fix default value in config template

- samesite = false, not none

* Remove conflicting migration

- Should not have back-ported this from master branch
- Will not cause any serious issues, was a "nice to have" data migration
2024-10-29 10:17:41 +11:00
github-actions[bot]
343f63c6ba [Bug] Ensure links are prepended with base URL on receipt (#8367) (#8370)
* Ensure links are prepended with base URL on receipt

* Bug fix

(cherry picked from commit 3253a4a93c)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-26 12:49:16 +11:00
Oliver
2bccbffe56 Update version.py (#8364)
Bump version number to 0.16.7
2024-10-26 12:19:40 +11:00
Oliver
5af0e50b79 Bug fix for attachment updating (#8362)
-  Closes https://github.com/inventree/InvenTree/issues/8354
2024-10-25 20:20:23 +11:00
github-actions[bot]
0ae9cdd39f Notifications fix (#8360) (#8361)
* Fix for app loading

- Allow collection for background worker too

* Improved logging

* Refactor MethodStorageClass

- Cache methods more intelligently
- Re-collect if null

(cherry picked from commit 331692bb46)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-25 18:02:43 +11:00
github-actions[bot]
7babef098a Add "active" field to SupplierPart form (#8341) (#8342)
- Previously missing from legacy interface

(cherry picked from commit 295f733ed9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-23 13:07:08 +11:00
github-actions[bot]
fab846e3cc Markdown link fix (#8328) (#8329)
* Improve cleaning of markdown content

* Update unit test with new check

(cherry picked from commit cb0248d159)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-22 13:17:04 +11:00
github-actions[bot]
d485c6796b Add documentation about user management (#8321) (#8322)
(cherry picked from commit e219b7c914)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-21 10:24:59 +11:00
github-actions[bot]
5c94366bb5 Clear allocations when manually returning an item into stock from a customer (#8298) (#8300)
(cherry picked from commit 181e1dd9cc)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-16 18:39:11 +11:00
github-actions[bot]
cebad3d142 [CUI] Fix rendering issues for barcodes (#8286) (#8288)
- Prevent barcode data from being "escaped"
- Run through bleach to brevent malicious data injection

(cherry picked from commit b1d9a23928)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-15 17:28:40 +11:00
github-actions[bot]
3659bbe389 Fix bug in merge_stock_items (#8284) (#8285)
- self() -> self

(cherry picked from commit 14d92b8727)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-10-15 14:22:49 +11:00
Oliver
933a5a5595 Fix for COOKIE_MODE in settings.py (#8268)
- Backport of https://github.com/inventree/InvenTree/pull/8262
2024-10-10 09:51:48 +11:00
github-actions[bot]
6c0f6e38d0 fix part qr lable (#8255) (#8256)
(cherry picked from commit 560f57333c)

Co-authored-by: Volker <skydiablo@gmx.net>
2024-10-08 21:06:21 +11:00
Oliver
8c9a438e59 Update version.py (#8248) 2024-10-07 20:52:08 +11:00
Oliver
6e37f0cd8b Markdown xss backport (#8244)
* Update helpers.py

* Update mixins.py

* format

* format

* Allow horizontal rule in markdown

* More instructive error msg

* Specify output_format to markdown.markdown

Ref: https://python-markdown.github.io/reference/markdown/serializers/

* Cleanup

* Adjust allowable markdown tags

* Add unit test for malicious markdown XSS

* Allow <pre> tag

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2024-10-07 20:03:39 +11:00
github-actions[bot]
1c6d25ce33 Fix build item over-allocation checks (#8235) (#8241)
(cherry picked from commit a1024f1a67)

Co-authored-by: Dean <me@dgardiner.net>
2024-10-06 21:13:27 +11:00
Oliver
86111ad9b9 Merge commit from fork (#8229)
* Sanitize markdown when rendering notes fields

* Revert "Sanitize markdown when rendering notes fields"

This reverts commit 373ebfbef0ce25fa58cb4e635e99f35b5e4df07c.

* Mask error message when downloading image
2024-10-02 14:28:13 +10:00
Oliver
524e6ddf79 Update version.py (#8214)
Bump version number to 0.16.5
2024-09-30 10:36:24 +10:00
github-actions[bot]
83be1b8a0f Path management improvements (#8210) (#8212)
- Improve path resolution for backup and restore commands
- Closes https://github.com/inventree/InvenTree/issues/8207

(cherry picked from commit 73a3e504a9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-09-30 10:34:38 +10:00
github-actions[bot]
974c2737af Add exception handling for default template creatoin (#8209) (#8211)
(cherry picked from commit a71754b086)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-09-30 10:19:36 +10:00
github-actions[bot]
0a0da7b65b Exception handling for BulkDeleteMixin (#8205) (#8206)
* Exception handling for BulkDeleteMixin

* Fix unit test

(cherry picked from commit 33499d61bd)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-09-29 15:38:28 +10:00
Oliver
b12bd3bb4b Ensure that stock item trees are rebuilt correctly after serialization (#8193)
- No idea how this has not been detected previously
2024-09-26 22:54:45 +10:00
Oliver
83be3cfa71 Increase timeout for report printing (#8187) 2024-09-26 09:48:37 +10:00
github-actions[bot]
fda47ff6ee Fix typo (#8181) (#8182)
direction -> directory

(cherry picked from commit 0faa507a14)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-09-25 12:07:45 +10:00
github-actions[bot]
69676f308b Remove translation for logged warnings (#8173) (#8176)
(cherry picked from commit 8928bc127a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-09-24 21:52:31 +10:00
github-actions[bot]
178e3313f9 Enhance exception management (#8174) (#8175)
(cherry picked from commit 6d0353028f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-09-24 21:52:03 +10:00
Oliver
b0353fafbf Update version.py (#8158)
Bump version number to 0.16.4
2024-09-21 18:29:33 +10:00
47 changed files with 521 additions and 194 deletions

View File

@@ -83,6 +83,26 @@ For more information, refer to the installation guides:
!!! warning "Invoke Update"
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
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.

View 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.

View File

@@ -46,6 +46,14 @@ Environment variable settings generally use the `INVENTREE_` prefix, and are all
!!! warning "Available Variables"
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
The following basic options are available:

View File

@@ -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.
!!! 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"
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"
For a production setup you do not need the InvenTree source code. Simply download the three required files from the links above!

View File

@@ -136,17 +136,19 @@ def define_env(env):
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

View File

@@ -97,6 +97,7 @@ nav:
- Production: start/bare_prod.md
- Development: start/bare_dev.md
- Serving Files: start/serving_files.md
- User Accounts: start/accounts.md
- Data Backup: start/backup.md
- Migrating Data: start/migrate.md
- Advanced Topics: start/advanced.md

View File

@@ -17,6 +17,9 @@
},
{
"pattern": "https://www.reddit.com/r/InvenTree/"
},
{
"pattern": "https://opensource.org/"
}
]
}

View File

@@ -383,11 +383,26 @@ class BulkDeleteMixin:
# Filter by provided item ID values
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
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)
self.validate_delete(queryset, request)

View File

@@ -40,9 +40,14 @@ class InvenTreeConfig(AppConfig):
- Adding users set in the current environment
"""
# 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 (
not InvenTree.ready.isPluginRegistryLoaded()
or not InvenTree.ready.isInMainThread()
not InvenTree.ready.isInMainThread()
and not InvenTree.ready.isInWorkerThread()
):
return
@@ -52,7 +57,6 @@ class InvenTreeConfig(AppConfig):
if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV:
self.remove_obsolete_tasks()
self.collect_tasks()
self.start_background_tasks()

View File

@@ -3,7 +3,6 @@
import datetime
import hashlib
import io
import json
import logging
import os
import os.path
@@ -21,13 +20,11 @@ from django.core.files.storage import Storage, default_storage
from django.http import StreamingHttpResponse
from django.utils.translation import gettext_lazy as _
import bleach
import pytz
import regex
from bleach import clean
from djmoney.money import Money
from PIL import Image
import InvenTree.version
from common.currency import currency_code_default
from .settings import MEDIA_URL, STATIC_URL
@@ -143,6 +140,8 @@ def getStaticUrl(filename):
def TestIfImage(img):
"""Test if an image file is indeed an image."""
from PIL import Image
try:
Image.open(img).verify()
return True
@@ -784,32 +783,85 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
return cleaned
def remove_non_printable_characters(
value: str, remove_newline=True, remove_ascii=True, remove_unicode=True
):
def remove_non_printable_characters(value: str, remove_newline=True) -> str:
"""Remove non-printable / control characters from the provided string."""
cleaned = value
if remove_ascii:
# Remove ASCII control characters
# Note that we do not sub out 0x0A (\n) here, it is done separately below
cleaned = regex.sub('[\x00-\x09]+', '', cleaned)
cleaned = regex.sub('[\x0b-\x1f\x7f]+', '', cleaned)
# Remove ASCII control characters
# 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('', cleaned)
# Remove Unicode control characters
regex = re.compile(r'[\u200E\u200F\u202A-\u202E]')
cleaned = regex.sub('', cleaned)
if remove_newline:
cleaned = regex.sub('[\x0a]+', '', 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)
regex = re.compile(r'[\x0A]')
cleaned = regex.sub('', 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):
"""Calculate a 'unique' hash for a barcode string.

View File

@@ -6,7 +6,11 @@ from rest_framework import generics, mixins, status
from rest_framework.response import Response
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:
@@ -57,6 +61,7 @@ class CleanMixin:
# By default, newline characters are removed
remove_newline = True
is_markdown = False
try:
if hasattr(self, 'serializer_class'):
@@ -64,11 +69,12 @@ class CleanMixin:
field = model._meta.get_field(field)
# The following field types allow newline characters
allow_newline = [InvenTreeNotesField]
allow_newline = [(InvenTreeNotesField, True)]
for field_type in allow_newline:
if issubclass(type(field), field_type):
if issubclass(type(field), field_type[0]):
remove_newline = False
is_markdown = field_type[1]
break
except AttributeError:
@@ -80,6 +86,9 @@ class CleanMixin:
cleaned, remove_newline=remove_newline
)
if is_markdown:
cleaned = clean_markdown(cleaned)
return cleaned
def clean_data(self, data: dict) -> dict:

View File

@@ -882,8 +882,8 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
try:
self.remote_image_file = download_image_from_url(url)
except Exception as exc:
except Exception:
self.remote_image_file = None
raise ValidationError(str(exc))
raise ValidationError(_('Failed to download image from remote URL'))
return url

View File

@@ -33,7 +33,8 @@ from . import config, locales
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"
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
if TESTING and '--exclude-tag=cui' in sys.argv:
ENABLE_CLASSIC_FRONTEND = False
ENABLE_PLATFORM_FRONTEND = get_boolean_setting(
'INVENTREE_PLATFORM_FRONTEND', 'platform_frontend', True
)
@@ -1060,26 +1062,40 @@ if (
sys.exit(-1)
COOKIE_MODE = (
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'None'))
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'False'))
.lower()
.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():
logger.error('Invalid cookie samesite mode: %s', COOKIE_MODE)
sys.exit(-1)
COOKIE_MODE = valid_cookie_modes[COOKIE_MODE.lower()]
if not DEBUG and not TESTING and COOKIE_MODE in valid_cookie_modes:
# Set the cookie mode (in production mode only)
COOKIE_MODE = COOKIE_MODE.capitalize()
else:
# Default to False, as per the Django settings
COOKIE_MODE = False
# Additional CSRF settings
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_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(
@@ -1231,23 +1247,29 @@ MARKDOWNIFY = {
'abbr',
'b',
'blockquote',
'code',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'hr',
'i',
'img',
'li',
'ol',
'p',
'pre',
's',
'strong',
'ul',
'table',
'thead',
'tbody',
'th',
'tr',
'td',
'ul',
],
}
}

View File

@@ -5,7 +5,6 @@ import logging
from datetime import timedelta
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_q.models import Success
from django_q.status import Stat
@@ -63,13 +62,13 @@ def check_system_health(**kwargs):
if not is_worker_running(**kwargs): # pragma: no cover
result = False
logger.warning(_('Background worker check failed'))
logger.warning('Background worker check failed')
if not InvenTree.email.is_email_configured(): # pragma: no cover
result = False
logger.warning(_('Email backend not configured'))
logger.warning('Email backend not configured')
if not result: # pragma: no cover
logger.warning(_('InvenTree system health checks failed'))
logger.warning('InvenTree system health checks failed')
return result

View File

@@ -670,3 +670,15 @@ def admin_url(user, table, pk):
pass
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

View File

@@ -504,14 +504,14 @@ urlpatterns.append(
)
)
# Send any unknown URLs to the parts page
# Send any unknown URLs to the index page
urlpatterns += [
re_path(
r'^.*$',
RedirectView.as_view(
url='/index/'
if settings.ENABLE_CLASSIC_FRONTEND
else settings.FRONTEND_URL_BASE,
else f'/{settings.FRONTEND_URL_BASE}/',
permanent=False,
),
name='index',

View File

@@ -18,7 +18,7 @@ from django.conf import settings
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = '0.16.3'
INVENTREE_SW_VERSION = '0.16.9'
logger = logging.getLogger('inventree')

View File

@@ -1502,12 +1502,19 @@ class BuildItem(InvenTree.models.InvenTreeMetadataModel):
'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)
allocated = decimal.Decimal(self.stock_item.allocation_count())
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({
'quantity': _('Stock item is over-allocated')
})

View File

@@ -3,6 +3,7 @@
{% load static %}
{% load i18n %}
{% load generic %}
{% load barcode %}
{% load inventree_extras %}
{% block page_title %}
@@ -277,7 +278,7 @@ src="{% static 'img/blank_image.png' %}"
$('#show-qr-code').click(function() {
showQRDialog(
'{% trans "Build Order QR Code" escape %}',
'{{ build.barcode }}'
`{% clean_barcode build.barcode %}`
);
});

View File

@@ -224,7 +224,7 @@ class BuildTest(BuildAPITest):
"status": 50, # Item requires attention
},
expected_code=201,
max_query_count=450, # TODO: Try to optimize this
max_query_count=600,
)
self.assertEqual(self.build.incomplete_outputs.count(), 0)
@@ -920,6 +920,65 @@ class BuildAllocationTest(BuildAPITest):
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):
"""Unit tests for over allocation of stock items against a build order.

View File

@@ -29,5 +29,5 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(set_default_currency),
migrations.RunPython(set_default_currency, reverse_code=migrations.RunPython.noop),
]

View File

@@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
import common.models
import InvenTree.helpers
from InvenTree.ready import isImportingData
from InvenTree.ready import isImportingData, isRebuildingData
from plugin import registry
from plugin.models import NotificationUserSetting, PluginConfig
from users.models import Owner
@@ -185,9 +185,20 @@ class MethodStorageClass:
Is initialized on startup as one instance named `storage` in this file.
"""
liste = None
methods_list = None
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):
"""Collect all classes in the environment that are notification methods.
@@ -196,7 +207,8 @@ class MethodStorageClass:
Args:
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 = (
InvenTree.helpers.inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS
)
@@ -219,8 +231,12 @@ class MethodStorageClass:
item.plugin = plugin() if plugin else None
filtered_list[ref] = item
storage.liste = list(filtered_list.values())
logger.info('Found %s notification methods', len(storage.liste))
storage.methods_list = list(filtered_list.values())
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:
"""Returns all user settings for a specific user.
@@ -234,7 +250,8 @@ class MethodStorageClass:
list: All applicablae notification settings.
"""
methods = []
for item in storage.liste:
for item in storage.methods:
if item.USER_SETTING:
new_key = f'NOTIFICATION_METHOD_{item.METHOD_NAME.upper()}'
@@ -250,6 +267,7 @@ class MethodStorageClass:
'icon': getattr(item, 'METHOD_ICON', ''),
'method': item.METHOD_NAME,
})
return methods
@@ -352,7 +370,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
delivery_methods = kwargs.get('delivery_methods', None)
# Check if data is importing currently
if isImportingData():
if isImportingData() or isRebuildingData():
return
# Resolve object reference
@@ -422,7 +440,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
# Collect possible methods
if delivery_methods is None:
delivery_methods = storage.liste or []
delivery_methods = storage.methods or []
else:
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
@@ -439,7 +457,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
# Set delivery flag
common.models.NotificationEntry.notify(category, obj_ref_value)
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):

View File

@@ -546,6 +546,10 @@ class AttachmentSerializer(InvenTreeModelSerializer):
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
user = self.context.get('request').user

View File

@@ -70,6 +70,10 @@ def update_news_feed():
if entry.id in id_list:
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
try:
NewsFeedEntry.objects.create(

View File

@@ -1,6 +1,7 @@
{% extends "page_base.html" %}
{% load static %}
{% load i18n %}
{% load barcode %}
{% load inventree_extras %}
{% block page_title %}
@@ -303,7 +304,7 @@ onPanelLoad('supplier-part-notes', function() {
$("#show-qr-code").click(function() {
showQRDialog(
'{% trans "Supplier Part QR Code" escape %}',
'{{ part.barcode }}'
`{% clean_barcode part.barcode %}`
);
});

View File

@@ -156,6 +156,52 @@ class CompanyTest(InvenTreeAPITestCase):
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))',
'![x](javascript:alert(123))',
'![Uh oh...]("onerror="alert(\'XSS\'))',
]
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 ![image](https://www.google.com/test.jpg)',
'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):
"""Tests for the Contact models."""

View File

@@ -117,7 +117,7 @@ use_x_forwarded_port: false
# Cookie settings
cookie:
secure: false
samesite: none
samesite: false
# Cross Origin Resource Sharing (CORS) settings (see https://github.com/adamchainz/django-cors-headers)
cors:

View File

@@ -1098,6 +1098,8 @@ class SalesOrder(TotalPriceMixin, Order):
self.status = SalesOrderStatus.COMPLETE.value
else:
self.status = SalesOrderStatus.SHIPPED.value
if self.shipment_date is None:
self.shipped_by = user
self.shipment_date = InvenTree.helpers.current_date()
@@ -2196,7 +2198,7 @@ class ReturnOrder(TotalPriceMixin, Order):
# endregion
@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.
Rules:
@@ -2222,7 +2224,7 @@ class ReturnOrder(TotalPriceMixin, Order):
deltas['customer'] = stock_item.customer.pk
# Update the StockItem
stock_item.status = StockStatus.QUARANTINED.value
stock_item.status = kwargs.get('status', StockStatus.QUARANTINED.value)
stock_item.location = location
stock_item.customer = None
stock_item.sales_order = None

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load static %}
{% load barcode %}
{% load inventree_extras %}
{% load generic %}
@@ -312,7 +313,7 @@ $("#export-order").click(function() {
$('#show-qr-code').click(function() {
showQRDialog(
'{% trans "Purchase Order QR Code" escape %}',
'{{ order.barcode }}'
`{% clean_barcode order.barcode %}`
);
});

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load static %}
{% load barcode %}
{% load inventree_extras %}
{% load generic %}
@@ -257,7 +258,7 @@ $('#print-order-report').click(function() {
$('#show-qr-code').click(function() {
showQRDialog(
'{% trans "Return Order QR Code" escape %}',
'{{ order.barcode }}'
`{% clean_barcode order.barcode %}`
);
});

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load static %}
{% load barcode %}
{% load inventree_extras %}
{% load generic %}
@@ -319,7 +320,7 @@ $('#print-order-report').click(function() {
$('#show-qr-code').click(function() {
showQRDialog(
'{% trans "Sales Order QR Code" escape %}',
'{{ order.barcode }}'
`{% clean_barcode order.barcode %}`
);
});

View File

@@ -1576,6 +1576,8 @@ class SalesOrderTest(OrderTest):
so.refresh_from_db()
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
# 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"
so.status = SalesOrderStatus.PENDING.value
so.shipment_date = None
so.shipped_by = None
so.save()
so.refresh_from_db()
self.assertEqual(so.status, SalesOrderStatus.PENDING.value)
self.assertIsNone(so.shipped_by)
self.assertIsNone(so.shipment_date)
InvenTreeSetting.set_setting('SALESORDER_SHIP_COMPLETE', True)
@@ -1603,6 +1610,9 @@ class SalesOrderTest(OrderTest):
so.refresh_from_db()
self.assertEqual(so.status, SalesOrderStatus.COMPLETE.value)
self.assertIsNotNone(so.shipment_date)
self.assertIsNotNone(so.shipped_by)
class SalesOrderLineItemTest(OrderTest):
"""Tests for the SalesOrderLineItem API."""

View File

@@ -353,7 +353,8 @@ class LineItemPricing(PartPricing):
try:
part_id = self.request.POST.get('pk')
part = Part.objects.get(id=part_id)
except Part.DoesNotExist:
except Exception:
# Part not found, or invalid ID
return None
else:
return None

View File

@@ -2,6 +2,7 @@
{% load static %}
{% load i18n %}
{% load barcode %}
{% load inventree_extras %}
{% block sidebar %}
@@ -451,7 +452,7 @@
$("#show-qr-code").click(function() {
showQRDialog(
'{% trans "Part QR Code" escape %}',
'{{ part.barcode }}',
`{% clean_barcode part.barcode %}`
);
});

View File

@@ -95,7 +95,7 @@ def notification_list(context, *args, **kwargs):
'description': a.__doc__,
'name': a.__name__,
}
for a in storage.liste
for a in storage.methods
]

View File

@@ -125,12 +125,14 @@ class ReportConfig(AppConfig):
# Read the existing template file
data = template_file.open('r').read()
logger.info("Creating new label template: '%s'", template['name'])
# Create a new entry
report.models.LabelTemplate.objects.create(
**template, template=ContentFile(data, os.path.basename(filename))
)
try:
# Create a new entry
report.models.LabelTemplate.objects.create(
**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):
"""Create default report templates."""
@@ -212,9 +214,11 @@ class ReportConfig(AppConfig):
# Read the existing template file
data = template_file.open('r').read()
logger.info("Creating new report template: '%s'", template['name'])
# Create a new entry
report.models.ReportTemplate.objects.create(
**template, template=ContentFile(data, os.path.basename(filename))
)
try:
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

View File

@@ -1,6 +1,7 @@
"""Template tags for rendering various barcodes."""
from django import template
from django.utils.safestring import mark_safe
import barcode as python_barcode
import qrcode.constants as ECL
@@ -26,6 +27,23 @@ def image_data(img, fmt='PNG'):
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()
def qrcode(data, **kwargs):
"""Return a byte-encoded QR code image.

View File

@@ -1161,10 +1161,12 @@ class StockItem(
location=location,
)
# Clear out allocation information for the stock item
self.customer = None
self.belongs_to = None
self.sales_order = None
self.location = location
self.clearAllocations()
trigger_event('stockitem.returnedfromcustomer', id=self.id)
@@ -1189,9 +1191,17 @@ class StockItem(
return False
def build_allocation_count(self):
"""Return the total quantity allocated to builds."""
query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
def build_allocation_count(self, **kwargs):
"""Return the total quantity allocated to builds, with optional filters."""
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']
@@ -1577,6 +1587,13 @@ class StockItem(
# Remove the equivalent number of items
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
def copyHistoryFrom(self, other):
"""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
for allocation in other.sales_order_allocations.all():
allocation.stock_item = self()
allocation.stock_item = self
allocation.save()
# Prevent atomicity issues when we are merging our own "parent" part in
@@ -1811,7 +1828,7 @@ class StockItem(
for tree_id in tree_ids:
StockItem.objects.partial_rebuild(tree_id=tree_id)
except Exception:
logger.warning('Rebuilding entire StockItem tree')
logger.warning('Rebuilding entire StockItem tree during merge_stock_items')
StockItem.objects.rebuild()
@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."""
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):
InvenTree.tasks.offload_task(
part_tasks.notify_low_stock_if_required, instance.part

View File

@@ -3,6 +3,7 @@
{% load plugin_extras %}
{% load inventree_extras %}
{% load generic %}
{% load barcode %}
{% load i18n %}
{% load l10n %}
@@ -534,7 +535,7 @@ $('#stock-edit-status').click(function () {
$("#show-qr-code").click(function() {
showQRDialog(
'{% trans "Stock Item QR Code" escape %}',
'{{ item.barcode }}',
`{% clean_barcode item.barcode %}`
);
});

View File

@@ -1,5 +1,6 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load barcode %}
{% load inventree_extras %}
{% load plugin_extras %}
{% load i18n %}
@@ -391,7 +392,7 @@
$('#show-qr-code').click(function() {
showQRDialog(
'{% trans "Stock Location QR Code" escape %}',
'{{ location.barcode }}'
`{% clean_barcode location.barcode %}`
);
});

View File

@@ -1713,7 +1713,7 @@ class StockTestResultTest(StockAPITestCase):
# Now, let's delete all the newly created items with a single API request
# However, we will provide incorrect filters
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)

View File

@@ -165,6 +165,7 @@ function supplierPartFields(options={}) {
icon: 'fa-box',
},
pack_quantity: {},
active: {},
};
if (options.part) {

View File

@@ -2,8 +2,9 @@
{% load i18n %}
{% 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'>
{% if mode == 'admin' %}
{% trans "Platform UI - the new UI for InvenTree - provides more modern administration options." %}

View File

@@ -47,7 +47,6 @@ python-dotenv # Environment variable management
pyyaml>=6.0.1 # YAML parsing
qrcode[pil] # QR code generator
rapidfuzz # Fuzzy string matching
regex # Advanced regular expressions
sentry-sdk # Error reporting (optional)
setuptools # Standard dependency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats

View File

@@ -6,9 +6,9 @@ asgiref==3.8.1 \
# via
# django
# django-cors-headers
async-timeout==4.0.3 \
--hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \
--hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028
async-timeout==5.0.1 \
--hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
--hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
# via redis
attrs==23.2.0 \
--hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \
@@ -1353,87 +1353,6 @@ referencing==0.35.1 \
# via
# jsonschema
# 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 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6

View File

@@ -92,7 +92,7 @@ export function PrintingActions({
url: apiUrl(ApiEndpoints.label_print),
title: t`Print Label`,
fields: labelFields,
timeout: (items.length + 1) * 1000,
timeout: (items.length + 1) * 5000,
onClose: () => {
setPluginKey('');
},
@@ -121,7 +121,7 @@ export function PrintingActions({
const reportModal = useCreateApiFormModal({
title: t`Print Report`,
url: apiUrl(ApiEndpoints.report_print),
timeout: (items.length + 1) * 1000,
timeout: (items.length + 1) * 5000,
fields: {
template: {
filters: {

View File

@@ -411,6 +411,11 @@ def backup(c, clean=False, path=None):
cmd = '--noinput --compress -v 2'
if path:
# Resolve the provided path
path = Path(path)
if not os.path.isabs(path):
path = localDir().joinpath(path).resolve()
cmd += f' -O {path}'
if clean:
@@ -442,6 +447,11 @@ def restore(
base_cmd = '--noinput --uncompress -v 2'
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}'
if ignore_database:
@@ -1418,11 +1428,11 @@ def docs_server(c, address='localhost:8080', compile_schema=False):
def clear_generated(c):
"""Clear generated files from `inv update`."""
# pyc/pyo files
run(c, 'find . -name "*.pyc" -exec rm -f {} +')
run(c, 'find . -name "*.pyo" -exec rm -f {} +')
run(c, 'find src -name "*.pyc" -exec rm -f {} +')
run(c, 'find src -name "*.pyo" -exec rm -f {} +')
# cache folders
run(c, 'find . -name "__pycache__" -exec rm -rf {} +')
run(c, 'find src -name "__pycache__" -exec rm -rf {} +')
# Generated translations
run(c, 'find . -name "django.mo" -exec rm -f {} +')
run(c, 'find . -name "messages.mo" -exec rm -f {} +')
run(c, 'find src -name "django.mo" -exec rm -f {} +')
run(c, 'find src -name "messages.mo" -exec rm -f {} +')