mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-23 09:35:30 +00:00
Merge remote-tracking branch 'matmair/master'
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
name: Update dependency files regularly
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
run: pip install -r requirements-dev.txt
|
||||||
|
- name: Update requirements.txt
|
||||||
|
run: pip-compile --output-file=requirements.txt requirements.in -U
|
||||||
|
- name: Update requirements-dev.txt
|
||||||
|
run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
with:
|
||||||
|
commit_message: "[Bot] Updated dependency"
|
||||||
|
branch: dep-update
|
||||||
+4
-3
@@ -7,12 +7,13 @@ tasks:
|
|||||||
export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static'
|
export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static'
|
||||||
export PIP_USER='no'
|
export PIP_USER='no'
|
||||||
|
|
||||||
sudo apt install gettext
|
sudo apt install -y gettext
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install invoke
|
pip install invoke pyyaml
|
||||||
mkdir dev
|
mkdir dev
|
||||||
inv setup-test
|
invoke update
|
||||||
|
invoke setup-test --ignore-update --path inventree-data
|
||||||
gp sync-done start_server
|
gp sync-done start_server
|
||||||
|
|
||||||
- name: Start server
|
- name: Start server
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 69
|
INVENTREE_API_VERSION = 70
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v70 -> 2022-08-02 : https://github.com/inventree/InvenTree/pull/3451
|
||||||
|
- Adds a 'depth' parameter to the PartCategory list API
|
||||||
|
- Adds a 'depth' parameter to the StockLocation list API
|
||||||
|
|
||||||
v69 -> 2022-08-01 : https://github.com/inventree/InvenTree/pull/3443
|
v69 -> 2022-08-01 : https://github.com/inventree/InvenTree/pull/3443
|
||||||
- Updates the PartCategory list API:
|
- Updates the PartCategory list API:
|
||||||
- Improve query efficiency: O(n) becomes O(1)
|
- Improve query efficiency: O(n) becomes O(1)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from decimal import Decimal
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.db import models as models
|
from django.db import models as models
|
||||||
from django.forms.fields import URLField as FormURLField
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from djmoney.forms.fields import MoneyField
|
from djmoney.forms.fields import MoneyField
|
||||||
@@ -23,26 +22,28 @@ class InvenTreeRestURLField(RestURLField):
|
|||||||
"""Custom field for DRF with custom scheme vaildators."""
|
"""Custom field for DRF with custom scheme vaildators."""
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Update schemes."""
|
"""Update schemes."""
|
||||||
|
|
||||||
|
# Enforce 'max length' parameter in form validation
|
||||||
|
if 'max_length' not in kwargs:
|
||||||
|
kwargs['max_length'] = 200
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.validators[-1].schemes = allowable_url_schemes()
|
self.validators[-1].schemes = allowable_url_schemes()
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeURLFormField(FormURLField):
|
|
||||||
"""Custom URL form field with custom scheme validators."""
|
|
||||||
|
|
||||||
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
|
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeURLField(models.URLField):
|
class InvenTreeURLField(models.URLField):
|
||||||
"""Custom URL field which has custom scheme validators."""
|
"""Custom URL field which has custom scheme validators."""
|
||||||
|
|
||||||
validators = [validators.URLValidator(schemes=allowable_url_schemes())]
|
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Return a Field instance for this field."""
|
"""Initialization method for InvenTreeURLField"""
|
||||||
return super().formfield(**{
|
|
||||||
'form_class': InvenTreeURLFormField
|
# Max length for InvenTreeURLField defaults to 200
|
||||||
})
|
if 'max_length' not in kwargs:
|
||||||
|
kwargs['max_length'] = 200
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def money_kwargs():
|
def money_kwargs():
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from wsgiref.util import FileWrapper
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.staticfiles.storage import StaticFilesStorage
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
@@ -241,17 +242,27 @@ def getLogoImage(as_file=False, custom=True):
|
|||||||
"""Return the path to the logo-file."""
|
"""Return the path to the logo-file."""
|
||||||
if custom and settings.CUSTOM_LOGO:
|
if custom and settings.CUSTOM_LOGO:
|
||||||
|
|
||||||
if as_file:
|
static_storage = StaticFilesStorage()
|
||||||
return f"file://{default_storage.path(settings.CUSTOM_LOGO)}"
|
|
||||||
else:
|
|
||||||
return default_storage.url(settings.CUSTOM_LOGO)
|
|
||||||
|
|
||||||
else:
|
if static_storage.exists(settings.CUSTOM_LOGO):
|
||||||
if as_file:
|
storage = static_storage
|
||||||
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
|
elif default_storage.exists(settings.CUSTOM_LOGO):
|
||||||
return f"file://{path}"
|
storage = default_storage
|
||||||
else:
|
else:
|
||||||
return getStaticUrl('img/inventree.png')
|
storage = None
|
||||||
|
|
||||||
|
if storage is not None:
|
||||||
|
if as_file:
|
||||||
|
return f"file://{storage.path(settings.CUSTOM_LOGO)}"
|
||||||
|
else:
|
||||||
|
return storage.url(settings.CUSTOM_LOGO)
|
||||||
|
|
||||||
|
# If we have got to this point, return the default logo
|
||||||
|
if as_file:
|
||||||
|
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
|
||||||
|
return f"file://{path}"
|
||||||
|
else:
|
||||||
|
return getStaticUrl('img/inventree.png')
|
||||||
|
|
||||||
|
|
||||||
def TestIfImageURL(url):
|
def TestIfImageURL(url):
|
||||||
@@ -283,6 +294,22 @@ def str2bool(text, test=True):
|
|||||||
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ]
|
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ]
|
||||||
|
|
||||||
|
|
||||||
|
def str2int(text, default=None):
|
||||||
|
"""Convert a string to int if possible
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Int like string
|
||||||
|
default: Return value if str is no int like
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Converted int value
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return int(text)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def is_bool(text):
|
def is_bool(text):
|
||||||
"""Determine if a string value 'looks' like a boolean."""
|
"""Determine if a string value 'looks' like a boolean."""
|
||||||
if str2bool(text, True):
|
if str2bool(text, True):
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
model_class = None
|
model_class = None
|
||||||
|
|
||||||
|
# Attributes to copy extra attributes from the model to the field (if they don't exist)
|
||||||
|
extra_attributes = [
|
||||||
|
'help_text',
|
||||||
|
'max_length',
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_class = serializer.Meta.model
|
model_class = serializer.Meta.model
|
||||||
|
|
||||||
@@ -148,10 +154,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
elif name in model_default_values:
|
elif name in model_default_values:
|
||||||
serializer_info[name]['default'] = model_default_values[name]
|
serializer_info[name]['default'] = model_default_values[name]
|
||||||
|
|
||||||
# Attributes to copy from the model to the field (if they don't exist)
|
for attr in extra_attributes:
|
||||||
attributes = ['help_text']
|
|
||||||
|
|
||||||
for attr in attributes:
|
|
||||||
if attr not in serializer_info[name]:
|
if attr not in serializer_info[name]:
|
||||||
|
|
||||||
if hasattr(field, attr):
|
if hasattr(field, attr):
|
||||||
@@ -172,8 +175,9 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
# This is used to automatically filter AJAX requests
|
# This is used to automatically filter AJAX requests
|
||||||
serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to()
|
serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to()
|
||||||
|
|
||||||
if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'):
|
for attr in extra_attributes:
|
||||||
serializer_info[name]['help_text'] = relation.model_field.help_text
|
if attr not in serializer_info[name] and hasattr(relation.model_field, attr):
|
||||||
|
serializer_info[name][attr] = getattr(relation.model_field, attr)
|
||||||
|
|
||||||
if name in model_default_values:
|
if name in model_default_values:
|
||||||
serializer_info[name]['default'] = model_default_values[name]
|
serializer_info[name]['default'] = model_default_values[name]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import django.conf.locale
|
import django.conf.locale
|
||||||
|
from django.contrib.staticfiles.storage import StaticFilesStorage
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -127,7 +128,7 @@ STATFILES_I18_PROCESSORS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Color Themes Directory
|
# Color Themes Directory
|
||||||
STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes')
|
STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes').resolve()
|
||||||
|
|
||||||
# Web URL endpoint for served media files
|
# Web URL endpoint for served media files
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
@@ -135,6 +136,8 @@ MEDIA_URL = '/media/'
|
|||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
# Admin site integration
|
||||||
|
'django.contrib.admin',
|
||||||
|
|
||||||
# InvenTree apps
|
# InvenTree apps
|
||||||
'build.apps.BuildConfig',
|
'build.apps.BuildConfig',
|
||||||
@@ -150,7 +153,6 @@ INSTALLED_APPS = [
|
|||||||
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||||
|
|
||||||
# Core django modules
|
# Core django modules
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'user_sessions', # db user sessions
|
'user_sessions', # db user sessions
|
||||||
@@ -820,10 +822,22 @@ CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
|
|||||||
|
|
||||||
CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', None)
|
CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', None)
|
||||||
|
|
||||||
# check that the logo-file exsists in media
|
"""
|
||||||
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO): # pragma: no cover
|
Check for the existence of a 'custom logo' file:
|
||||||
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage")
|
- Check the 'static' directory
|
||||||
CUSTOM_LOGO = False
|
- Check the 'media' directory (legacy)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if CUSTOM_LOGO:
|
||||||
|
static_storage = StaticFilesStorage()
|
||||||
|
|
||||||
|
if static_storage.exists(CUSTOM_LOGO):
|
||||||
|
logger.info(f"Loading custom logo from static directory: {CUSTOM_LOGO}")
|
||||||
|
elif default_storage.exists(CUSTOM_LOGO):
|
||||||
|
logger.info(f"Loading custom logo from media directory: {CUSTOM_LOGO}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the static or media directories")
|
||||||
|
CUSTOM_LOGO = False
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
logger.info("InvenTree running with DEBUG enabled")
|
logger.info("InvenTree running with DEBUG enabled")
|
||||||
|
|||||||
@@ -119,3 +119,16 @@ class ViewTests(InvenTreeTestCase):
|
|||||||
for panel in staff_panels + plugin_panels:
|
for panel in staff_panels + plugin_panels:
|
||||||
self.assertNotIn(f"select-{panel}", content)
|
self.assertNotIn(f"select-{panel}", content)
|
||||||
self.assertNotIn(f"panel-{panel}", content)
|
self.assertNotIn(f"panel-{panel}", content)
|
||||||
|
|
||||||
|
def test_url_login(self):
|
||||||
|
"""Test logging in via arguments"""
|
||||||
|
|
||||||
|
# Log out
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.get("/index/")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Try login with url
|
||||||
|
response = self.client.get(f"/accounts/login/?next=/&login={self.username}&password={self.password}")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, '/')
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from users.api import user_urls
|
|||||||
|
|
||||||
from .api import InfoView, NotFoundView
|
from .api import InfoView, NotFoundView
|
||||||
from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView,
|
from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView,
|
||||||
CustomConnectionsView, CustomEmailView,
|
CustomConnectionsView, CustomEmailView, CustomLoginView,
|
||||||
CustomPasswordResetFromKeyView,
|
CustomPasswordResetFromKeyView,
|
||||||
CustomSessionDeleteOtherView, CustomSessionDeleteView,
|
CustomSessionDeleteOtherView, CustomSessionDeleteView,
|
||||||
CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView,
|
CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView,
|
||||||
@@ -168,6 +168,9 @@ frontendpatterns = [
|
|||||||
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
|
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
|
||||||
re_path(r'^accounts/two_factor/remove/?$', CustomTwoFactorRemove.as_view(), name='two-factor-remove'),
|
re_path(r'^accounts/two_factor/remove/?$', CustomTwoFactorRemove.as_view(), name='two-factor-remove'),
|
||||||
|
|
||||||
|
# Override login page
|
||||||
|
re_path("accounts/login/", CustomLoginView.as_view(), name="account_login"),
|
||||||
|
|
||||||
re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support
|
re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support
|
||||||
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
|
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ from django.views.generic.base import RedirectView, TemplateView
|
|||||||
|
|
||||||
from allauth.account.forms import AddEmailForm
|
from allauth.account.forms import AddEmailForm
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from allauth.account.views import EmailView, PasswordResetFromKeyView
|
from allauth.account.views import (EmailView, LoginView,
|
||||||
|
PasswordResetFromKeyView)
|
||||||
from allauth.socialaccount.forms import DisconnectForm
|
from allauth.socialaccount.forms import DisconnectForm
|
||||||
from allauth.socialaccount.views import ConnectionsView
|
from allauth.socialaccount.views import ConnectionsView
|
||||||
from allauth_2fa.views import TwoFactorRemove
|
from allauth_2fa.views import TwoFactorRemove
|
||||||
@@ -700,6 +701,23 @@ class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLoginView(LoginView):
|
||||||
|
"""Custom login view that allows login with urlargs."""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""Extendend get to allow for auth via url args."""
|
||||||
|
# Check if login is present
|
||||||
|
if 'login' in request.GET:
|
||||||
|
# Initiate form
|
||||||
|
form = self.get_form_class()(request.GET.dict(), request=request)
|
||||||
|
|
||||||
|
# Try to login
|
||||||
|
form.full_clean()
|
||||||
|
return form.login(request)
|
||||||
|
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CurrencyRefreshView(RedirectView):
|
class CurrencyRefreshView(RedirectView):
|
||||||
"""POST endpoint to refresh / update exchange rates."""
|
"""POST endpoint to refresh / update exchange rates."""
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import hmac
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import os
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -877,6 +876,16 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'default': True,
|
'default': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'INVENTREE_TREE_DEPTH': {
|
||||||
|
'name': _('Tree Depth'),
|
||||||
|
'description': _('Default tree depth for treeview. Deeper levels can be lazy loaded as they are needed.'),
|
||||||
|
'default': 1,
|
||||||
|
'validator': [
|
||||||
|
int,
|
||||||
|
MinValueValidator(0),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
'BARCODE_ENABLE': {
|
'BARCODE_ENABLE': {
|
||||||
'name': _('Barcode Support'),
|
'name': _('Barcode Support'),
|
||||||
'description': _('Enable barcode scanner support'),
|
'description': _('Enable barcode scanner support'),
|
||||||
@@ -1775,14 +1784,15 @@ class ColorTheme(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_color_themes_choices(cls):
|
def get_color_themes_choices(cls):
|
||||||
"""Get all color themes from static folder."""
|
"""Get all color themes from static folder."""
|
||||||
if settings.TESTING and not os.path.exists(settings.STATIC_COLOR_THEMES_DIR):
|
if not settings.STATIC_COLOR_THEMES_DIR.exists():
|
||||||
logger.error('Theme directory does not exsist')
|
logger.error('Theme directory does not exsist')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get files list from css/color-themes/ folder
|
# Get files list from css/color-themes/ folder
|
||||||
files_list = []
|
files_list = []
|
||||||
for file in os.listdir(settings.STATIC_COLOR_THEMES_DIR):
|
|
||||||
files_list.append(os.path.splitext(file))
|
for file in settings.STATIC_COLOR_THEMES_DIR.iterdir():
|
||||||
|
files_list.append([file.stem, file.suffix])
|
||||||
|
|
||||||
# Get color themes choices (CSS sheets)
|
# Get color themes choices (CSS sheets)
|
||||||
choices = [(file_name.lower(), _(file_name.replace('-', ' ').title()))
|
choices = [(file_name.lower(), _(file_name.replace('-', ' ').title()))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1376,7 +1376,7 @@ class SalesOrderAllocation(models.Model):
|
|||||||
|
|
||||||
# TODO: The logic here needs improving. Do we need to subtract our own amount, or something?
|
# TODO: The logic here needs improving. Do we need to subtract our own amount, or something?
|
||||||
if self.item.quantity - self.item.allocation_count() + self.quantity < self.quantity:
|
if self.item.quantity - self.item.allocation_count() + self.quantity < self.quantity:
|
||||||
errors['quantity'] = _('StockItem is over-allocated')
|
errors['quantity'] = _('Stock item is over-allocated')
|
||||||
|
|
||||||
if self.quantity <= 0:
|
if self.quantity <= 0:
|
||||||
errors['quantity'] = _('Allocation quantity must be greater than zero')
|
errors['quantity'] = _('Allocation quantity must be greater than zero')
|
||||||
|
|||||||
+10
-1
@@ -25,7 +25,8 @@ from company.models import Company, ManufacturerPart, SupplierPart
|
|||||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||||
ListCreateDestroyAPIView)
|
ListCreateDestroyAPIView)
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.helpers import DownloadFile, increment, isNull, str2bool
|
from InvenTree.helpers import (DownloadFile, increment, isNull, str2bool,
|
||||||
|
str2int)
|
||||||
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
|
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
|
||||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
|
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
|
||||||
UpdateAPI)
|
UpdateAPI)
|
||||||
@@ -85,6 +86,8 @@ class CategoryList(ListCreateAPI):
|
|||||||
|
|
||||||
cascade = str2bool(params.get('cascade', False))
|
cascade = str2bool(params.get('cascade', False))
|
||||||
|
|
||||||
|
depth = str2int(params.get('depth', None))
|
||||||
|
|
||||||
# Do not filter by category
|
# Do not filter by category
|
||||||
if cat_id is None:
|
if cat_id is None:
|
||||||
pass
|
pass
|
||||||
@@ -94,12 +97,18 @@ class CategoryList(ListCreateAPI):
|
|||||||
if not cascade:
|
if not cascade:
|
||||||
queryset = queryset.filter(parent=None)
|
queryset = queryset.filter(parent=None)
|
||||||
|
|
||||||
|
if cascade and depth is not None:
|
||||||
|
queryset = queryset.filter(level__lte=depth)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
category = PartCategory.objects.get(pk=cat_id)
|
category = PartCategory.objects.get(pk=cat_id)
|
||||||
|
|
||||||
if cascade:
|
if cascade:
|
||||||
parents = category.get_descendants(include_self=True)
|
parents = category.get_descendants(include_self=True)
|
||||||
|
if depth is not None:
|
||||||
|
parents = parents.filter(level__lte=category.level + depth)
|
||||||
|
|
||||||
parent_ids = [p.id for p in parents]
|
parent_ids = [p.id for p in parents]
|
||||||
|
|
||||||
queryset = queryset.filter(parent__in=parent_ids)
|
queryset = queryset.filter(parent__in=parent_ids)
|
||||||
|
|||||||
+30
-26
@@ -49,33 +49,36 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
"""Test the PartCategoryList API endpoint"""
|
"""Test the PartCategoryList API endpoint"""
|
||||||
url = reverse('api-part-category-list')
|
url = reverse('api-part-category-list')
|
||||||
|
|
||||||
response = self.get(url, expected_code=200)
|
# star categories manually for tests as it is not possible with fixures
|
||||||
|
# because the current user is no fixure itself and throws an invalid
|
||||||
|
# foreign key constrain
|
||||||
|
for pk in [3, 4]:
|
||||||
|
PartCategory.objects.get(pk=pk).set_starred(self.user, True)
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 8)
|
test_cases = [
|
||||||
|
({}, 8, 'no parameters'),
|
||||||
|
({'parent': 1, 'cascade': False}, 3, 'Filter by parent, no cascading'),
|
||||||
|
({'parent': 1, 'cascade': True}, 5, 'Filter by parent, cascading'),
|
||||||
|
({'cascade': True, 'depth': 0}, 8, 'Cascade with no parent, depth=0'),
|
||||||
|
({'cascade': False, 'depth': 10}, 8, 'Cascade with no parent, depth=0'),
|
||||||
|
({'parent': 'null', 'cascade': True, 'depth': 0}, 2, 'Cascade with null parent, depth=0'),
|
||||||
|
({'parent': 'null', 'cascade': True, 'depth': 10}, 8, 'Cascade with null parent and bigger depth'),
|
||||||
|
({'parent': 'null', 'cascade': False, 'depth': 10}, 2, 'No cascade even with depth specified with null parent'),
|
||||||
|
({'parent': 1, 'cascade': False, 'depth': 0}, 3, 'Dont cascade with depth=0 and parent'),
|
||||||
|
({'parent': 1, 'cascade': True, 'depth': 0}, 3, 'Cascade with depth=0 and parent'),
|
||||||
|
({'parent': 1, 'cascade': False, 'depth': 1}, 3, 'Dont cascade even with depth=1 specified with parent'),
|
||||||
|
({'parent': 1, 'cascade': True, 'depth': 1}, 5, 'Cascade with depth=1 with parent'),
|
||||||
|
({'parent': 1, 'cascade': True, 'depth': 'abcdefg'}, 5, 'Cascade with invalid depth and parent'),
|
||||||
|
({'parent': 42}, 8, 'Should return everything if parent_pk is not vaild'),
|
||||||
|
({'parent': 'null', 'exclude_tree': 1, 'cascade': True}, 2, 'Should return everything from except tree with pk=1'),
|
||||||
|
({'parent': 'null', 'exclude_tree': 42, 'cascade': True}, 8, 'Should return everything because exclude_tree=42 is no valid pk'),
|
||||||
|
({'parent': 1, 'starred': True, 'cascade': True}, 2, 'Should return the starred categories for the current user within the pk=1 tree'),
|
||||||
|
({'parent': 1, 'starred': False, 'cascade': True}, 3, 'Should return the not starred categories for the current user within the pk=1 tree'),
|
||||||
|
]
|
||||||
|
|
||||||
# Filter by parent, depth=1
|
for params, res_len, description in test_cases:
|
||||||
response = self.get(
|
response = self.get(url, params, expected_code=200)
|
||||||
url,
|
self.assertEqual(len(response.data), res_len, description)
|
||||||
{
|
|
||||||
'parent': 1,
|
|
||||||
'cascade': False,
|
|
||||||
},
|
|
||||||
expected_code=200
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 3)
|
|
||||||
|
|
||||||
# Filter by parent, cascading
|
|
||||||
response = self.get(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
'parent': 1,
|
|
||||||
'cascade': True,
|
|
||||||
},
|
|
||||||
expected_code=200,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 5)
|
|
||||||
|
|
||||||
# Check that the required fields are present
|
# Check that the required fields are present
|
||||||
fields = [
|
fields = [
|
||||||
@@ -90,9 +93,10 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
'url'
|
'url'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
response = self.get(url, expected_code=200)
|
||||||
for result in response.data:
|
for result in response.data:
|
||||||
for f in fields:
|
for f in fields:
|
||||||
self.assertIn(f, result)
|
self.assertIn(f, result, f'"{f}" is missing in result of PartCategory list')
|
||||||
|
|
||||||
def test_part_count(self):
|
def test_part_count(self):
|
||||||
"""Test that the 'part_count' field is annotated correctly"""
|
"""Test that the 'part_count' field is annotated correctly"""
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
|||||||
ListCreateDestroyAPIView)
|
ListCreateDestroyAPIView)
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
|
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
|
||||||
str2bool)
|
str2bool, str2int)
|
||||||
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
|
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
|
||||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||||
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
|
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
|
||||||
@@ -241,6 +241,8 @@ class StockLocationList(ListCreateAPI):
|
|||||||
|
|
||||||
cascade = str2bool(params.get('cascade', False))
|
cascade = str2bool(params.get('cascade', False))
|
||||||
|
|
||||||
|
depth = str2int(params.get('depth', None))
|
||||||
|
|
||||||
# Do not filter by location
|
# Do not filter by location
|
||||||
if loc_id is None:
|
if loc_id is None:
|
||||||
pass
|
pass
|
||||||
@@ -251,6 +253,9 @@ class StockLocationList(ListCreateAPI):
|
|||||||
if not cascade:
|
if not cascade:
|
||||||
queryset = queryset.filter(parent=None)
|
queryset = queryset.filter(parent=None)
|
||||||
|
|
||||||
|
if cascade and depth is not None:
|
||||||
|
queryset = queryset.filter(level__lte=depth)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -259,6 +264,9 @@ class StockLocationList(ListCreateAPI):
|
|||||||
# All sub-locations to be returned too?
|
# All sub-locations to be returned too?
|
||||||
if cascade:
|
if cascade:
|
||||||
parents = location.get_descendants(include_self=True)
|
parents = location.get_descendants(include_self=True)
|
||||||
|
if depth is not None:
|
||||||
|
parents = parents.filter(level__lte=location.level + depth)
|
||||||
|
|
||||||
parent_ids = [p.id for p in parents]
|
parent_ids = [p.id for p in parents]
|
||||||
queryset = queryset.filter(parent__in=parent_ids)
|
queryset = queryset.filter(parent__in=parent_ids)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.15 on 2022-08-07 02:38
|
||||||
|
|
||||||
|
import InvenTree.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0081_auto_20220801_0044'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=200, verbose_name='External Link'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -647,7 +647,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
|||||||
|
|
||||||
link = InvenTreeURLField(
|
link = InvenTreeURLField(
|
||||||
verbose_name=_('External Link'),
|
verbose_name=_('External Link'),
|
||||||
max_length=125, blank=True,
|
blank=True, max_length=200,
|
||||||
help_text=_("Link to external URL")
|
help_text=_("Link to external URL")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -54,11 +54,47 @@ class StockLocationTest(StockAPITestCase):
|
|||||||
StockLocation.objects.create(name='top', description='top category')
|
StockLocation.objects.create(name='top', description='top category')
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
"""Test StockLocation list."""
|
"""Test the StockLocationList API endpoint"""
|
||||||
# Check that we can request the StockLocation list
|
test_cases = [
|
||||||
response = self.client.get(self.list_url, format='json')
|
({}, 8, 'no parameters'),
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
({'parent': 1, 'cascade': False}, 2, 'Filter by parent, no cascading'),
|
||||||
self.assertGreaterEqual(len(response.data), 1)
|
({'parent': 1, 'cascade': True}, 2, 'Filter by parent, cascading'),
|
||||||
|
({'cascade': True, 'depth': 0}, 8, 'Cascade with no parent, depth=0'),
|
||||||
|
({'cascade': False, 'depth': 10}, 8, 'Cascade with no parent, depth=0'),
|
||||||
|
({'parent': 'null', 'cascade': True, 'depth': 0}, 7, 'Cascade with null parent, depth=0'),
|
||||||
|
({'parent': 'null', 'cascade': True, 'depth': 10}, 8, 'Cascade with null parent and bigger depth'),
|
||||||
|
({'parent': 'null', 'cascade': False, 'depth': 10}, 3, 'No cascade even with depth specified with null parent'),
|
||||||
|
({'parent': 1, 'cascade': False, 'depth': 0}, 2, 'Dont cascade with depth=0 and parent'),
|
||||||
|
({'parent': 1, 'cascade': True, 'depth': 0}, 2, 'Cascade with depth=0 and parent'),
|
||||||
|
({'parent': 1, 'cascade': False, 'depth': 1}, 2, 'Dont cascade even with depth=1 specified with parent'),
|
||||||
|
({'parent': 1, 'cascade': True, 'depth': 1}, 2, 'Cascade with depth=1 with parent'),
|
||||||
|
({'parent': 1, 'cascade': True, 'depth': 'abcdefg'}, 2, 'Cascade with invalid depth and parent'),
|
||||||
|
({'parent': 42}, 8, 'Should return everything if parent_pk is not vaild'),
|
||||||
|
({'parent': 'null', 'exclude_tree': 1, 'cascade': True}, 5, 'Should return everything except tree with pk=1'),
|
||||||
|
({'parent': 'null', 'exclude_tree': 42, 'cascade': True}, 8, 'Should return everything because exclude_tree=42 is no valid pk'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for params, res_len, description in test_cases:
|
||||||
|
response = self.get(self.list_url, params, expected_code=200)
|
||||||
|
self.assertEqual(len(response.data), res_len, description)
|
||||||
|
|
||||||
|
# Check that the required fields are present
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'level',
|
||||||
|
'parent',
|
||||||
|
'items',
|
||||||
|
'pathstring',
|
||||||
|
'owner',
|
||||||
|
'url'
|
||||||
|
]
|
||||||
|
|
||||||
|
response = self.get(self.list_url, expected_code=200)
|
||||||
|
for result in response.data:
|
||||||
|
for f in fields:
|
||||||
|
self.assertIn(f, result, f'"{f}" is missing in result of StockLocation list')
|
||||||
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
"""Test adding StockLocation."""
|
"""Test adding StockLocation."""
|
||||||
|
|||||||
@@ -44,6 +44,45 @@ class StockTest(InvenTreeTestCase):
|
|||||||
Part.objects.rebuild()
|
Part.objects.rebuild()
|
||||||
StockItem.objects.rebuild()
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
|
def test_link(self):
|
||||||
|
"""Test the link URL field validation"""
|
||||||
|
|
||||||
|
item = StockItem.objects.get(pk=1)
|
||||||
|
|
||||||
|
# Check that invalid URLs fail
|
||||||
|
for bad_url in [
|
||||||
|
'test.com',
|
||||||
|
'httpx://abc.xyz',
|
||||||
|
'https:google.com',
|
||||||
|
]:
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.link = bad_url
|
||||||
|
item.save()
|
||||||
|
item.full_clean()
|
||||||
|
|
||||||
|
# Check that valid URLs pass
|
||||||
|
for good_url in [
|
||||||
|
'https://test.com',
|
||||||
|
'https://digikey.com/datasheets?file=1010101010101.bin',
|
||||||
|
'ftp://download.com:8080/file.aspx',
|
||||||
|
]:
|
||||||
|
item.link = good_url
|
||||||
|
item.save()
|
||||||
|
item.full_clean()
|
||||||
|
|
||||||
|
# A long URL should fail
|
||||||
|
long_url = 'https://website.co.uk?query=' + 'a' * 173
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.link = long_url
|
||||||
|
item.full_clean()
|
||||||
|
|
||||||
|
# Shorten by a single character, will pass
|
||||||
|
long_url = long_url[:-1]
|
||||||
|
|
||||||
|
item.link = long_url
|
||||||
|
item.save()
|
||||||
|
|
||||||
def test_expiry(self):
|
def test_expiry(self):
|
||||||
"""Test expiry date functionality for StockItem model."""
|
"""Test expiry date functionality for StockItem model."""
|
||||||
today = datetime.datetime.now().date()
|
today = datetime.datetime.now().date()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE" icon="fa-server" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE" icon="fa-server" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_TREE_DEPTH" icon="fa-sitemap" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -1750,6 +1750,7 @@ function loadPartCategoryTable(table, options) {
|
|||||||
|
|
||||||
if (tree_view) {
|
if (tree_view) {
|
||||||
params.cascade = true;
|
params.cascade = true;
|
||||||
|
params.depth = global_settings.INVENTREE_TREE_DEPTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
var original = {};
|
var original = {};
|
||||||
@@ -1761,6 +1762,35 @@ function loadPartCategoryTable(table, options) {
|
|||||||
|
|
||||||
setupFilterList(filterKey, table, filterListElement);
|
setupFilterList(filterKey, table, filterListElement);
|
||||||
|
|
||||||
|
// Function to request sub-category items
|
||||||
|
function requestSubItems(parent_pk) {
|
||||||
|
inventreeGet(
|
||||||
|
options.url || '{% url "api-part-category-list" %}',
|
||||||
|
{
|
||||||
|
parent: parent_pk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
// Add the returned sub-items to the table
|
||||||
|
for (var idx = 0; idx < response.length; idx++) {
|
||||||
|
response[idx].parent = parent_pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = $(table).bootstrapTable('getRowByUniqueId', parent_pk);
|
||||||
|
row.subReceived = true;
|
||||||
|
|
||||||
|
$(table).bootstrapTable('updateByUniqueId', parent_pk, row, true);
|
||||||
|
|
||||||
|
table.bootstrapTable('append', response);
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
console.error('Error requesting sub-category for category=' + parent_pk);
|
||||||
|
showApiError(xhr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
table.inventreeTable({
|
table.inventreeTable({
|
||||||
treeEnable: tree_view,
|
treeEnable: tree_view,
|
||||||
rootParentId: tree_view ? options.params.parent : null,
|
rootParentId: tree_view ? options.params.parent : null,
|
||||||
@@ -1839,6 +1869,20 @@ function loadPartCategoryTable(table, options) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'load sub category' button
|
||||||
|
$(table).find('.load-sub-category').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const pk = $(this).attr('pk');
|
||||||
|
const row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
|
// Request sub-category for this category
|
||||||
|
requestSubItems(row.pk);
|
||||||
|
|
||||||
|
row.subRequested = true;
|
||||||
|
$(table).bootstrapTable('updateByUniqueId', pk, row, true);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
$('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
$('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
||||||
$('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
$('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
||||||
@@ -1859,8 +1903,20 @@ function loadPartCategoryTable(table, options) {
|
|||||||
switchable: true,
|
switchable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
var html = renderLink(
|
if (row._level >= global_settings.INVENTREE_TREE_DEPTH && !row.subReceived) {
|
||||||
|
if (row.subRequested) {
|
||||||
|
html += `<a href='#'><span class='fas fa-sync fa-spin'></span></a>`;
|
||||||
|
} else {
|
||||||
|
html += `
|
||||||
|
<a href='#' pk='${row.pk}' class='load-sub-category'>
|
||||||
|
<span class='fas fa-sync-alt' title='{% trans "Load Subcategories" %}'></span>
|
||||||
|
</a> `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += renderLink(
|
||||||
value,
|
value,
|
||||||
`/part/category/${row.pk}/`
|
`/part/category/${row.pk}/`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2226,6 +2226,7 @@ function loadStockLocationTable(table, options) {
|
|||||||
|
|
||||||
if (tree_view) {
|
if (tree_view) {
|
||||||
params.cascade = true;
|
params.cascade = true;
|
||||||
|
params.depth = global_settings.INVENTREE_TREE_DEPTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filters = {};
|
var filters = {};
|
||||||
@@ -2248,6 +2249,35 @@ function loadStockLocationTable(table, options) {
|
|||||||
filters[key] = params[key];
|
filters[key] = params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to request sub-location items
|
||||||
|
function requestSubItems(parent_pk) {
|
||||||
|
inventreeGet(
|
||||||
|
options.url || '{% url "api-location-list" %}',
|
||||||
|
{
|
||||||
|
parent: parent_pk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
// Add the returned sub-items to the table
|
||||||
|
for (var idx = 0; idx < response.length; idx++) {
|
||||||
|
response[idx].parent = parent_pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = $(table).bootstrapTable('getRowByUniqueId', parent_pk);
|
||||||
|
row.subReceived = true;
|
||||||
|
|
||||||
|
$(table).bootstrapTable('updateByUniqueId', parent_pk, row, true);
|
||||||
|
|
||||||
|
table.bootstrapTable('append', response);
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
console.error('Error requesting sub-locations for location=' + parent_pk);
|
||||||
|
showApiError(xhr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
table.inventreeTable({
|
table.inventreeTable({
|
||||||
treeEnable: tree_view,
|
treeEnable: tree_view,
|
||||||
rootParentId: tree_view ? options.params.parent : null,
|
rootParentId: tree_view ? options.params.parent : null,
|
||||||
@@ -2286,6 +2316,20 @@ function loadStockLocationTable(table, options) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'load sub location' button
|
||||||
|
$(table).find('.load-sub-location').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const pk = $(this).attr('pk');
|
||||||
|
const row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
|
// Request sub-location for this location
|
||||||
|
requestSubItems(row.pk);
|
||||||
|
|
||||||
|
row.subRequested = true;
|
||||||
|
$(table).bootstrapTable('updateByUniqueId', pk, row, true);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
$('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
$('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
||||||
$('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
$('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
||||||
@@ -2345,10 +2389,25 @@ function loadStockLocationTable(table, options) {
|
|||||||
switchable: true,
|
switchable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
return renderLink(
|
let html = '';
|
||||||
|
|
||||||
|
if (row._level >= global_settings.INVENTREE_TREE_DEPTH && !row.subReceived) {
|
||||||
|
if (row.subRequested) {
|
||||||
|
html += `<a href='#'><span class='fas fa-sync fa-spin'></span></a>`;
|
||||||
|
} else {
|
||||||
|
html += `
|
||||||
|
<a href='#' pk='${row.pk}' class='load-sub-location'>
|
||||||
|
<span class='fas fa-sync-alt' title='{% trans "Load Subloactions" %}'></span>
|
||||||
|
</a> `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += renderLink(
|
||||||
value,
|
value,
|
||||||
`/stock/location/${row.pk}/`
|
`/stock/location/${row.pk}/`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return html;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+21
-2
@@ -87,8 +87,16 @@ if __name__ == '__main__':
|
|||||||
# GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
|
# GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
|
||||||
GITHUB_REF = os.environ['GITHUB_REF']
|
GITHUB_REF = os.environ['GITHUB_REF']
|
||||||
|
|
||||||
|
GITHUB_REF_NAME = os.environ['GITHUB_REF_NAME']
|
||||||
|
|
||||||
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
|
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
|
||||||
|
|
||||||
|
# Print out version information, makes debugging actions *much* easier!
|
||||||
|
print(f"GITHUB_REF: {GITHUB_REF}")
|
||||||
|
print(f"GITHUB_REF_NAME: {GITHUB_REF_NAME}")
|
||||||
|
print(f"GITHUB_REF_TYPE: {GITHUB_REF_TYPE}")
|
||||||
|
print(f"GITHUB_BASE_REF: {GITHUB_BASE_REF}")
|
||||||
|
|
||||||
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
||||||
|
|
||||||
version = None
|
version = None
|
||||||
@@ -109,8 +117,19 @@ if __name__ == '__main__':
|
|||||||
print(f"InvenTree Version: '{version}'")
|
print(f"InvenTree Version: '{version}'")
|
||||||
|
|
||||||
# Check version number and look for existing versions
|
# Check version number and look for existing versions
|
||||||
# Note that on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
|
# If a release is found which matches the current tag, throw an error
|
||||||
highest_release = check_version_number(version, allow_duplicate=GITHUB_REF_TYPE == 'tag')
|
|
||||||
|
allow_duplicate = False
|
||||||
|
|
||||||
|
# Note: on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
|
||||||
|
if GITHUB_REF_TYPE == 'tag':
|
||||||
|
allow_duplicate = True
|
||||||
|
|
||||||
|
# Note: on a push to 'stable' branch we also allow duplicates
|
||||||
|
if GITHUB_BASE_REF == 'stable':
|
||||||
|
allow_duplicate = True
|
||||||
|
|
||||||
|
highest_release = check_version_number(version, allow_duplicate=allow_duplicate)
|
||||||
|
|
||||||
# Determine which docker tag we are going to use
|
# Determine which docker tag we are going to use
|
||||||
docker_tags = None
|
docker_tags = None
|
||||||
|
|||||||
+64
-56
@@ -10,6 +10,10 @@ asgiref==3.5.2 \
|
|||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# django
|
# django
|
||||||
|
build==0.8.0 \
|
||||||
|
--hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
|
||||||
|
--hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
|
||||||
|
# via pip-tools
|
||||||
certifi==2022.6.15 \
|
certifi==2022.6.15 \
|
||||||
--hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \
|
--hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \
|
||||||
--hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412
|
--hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412
|
||||||
@@ -20,9 +24,9 @@ cfgv==3.3.1 \
|
|||||||
--hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
|
--hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
|
||||||
--hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
|
--hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
charset-normalizer==2.0.12 \
|
charset-normalizer==2.1.0 \
|
||||||
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
|
--hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \
|
||||||
--hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df
|
--hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413
|
||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# requests
|
# requests
|
||||||
@@ -90,13 +94,13 @@ coveralls==2.1.2 \
|
|||||||
--hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \
|
--hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \
|
||||||
--hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1
|
--hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
distlib==0.3.4 \
|
distlib==0.3.5 \
|
||||||
--hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \
|
--hash=sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe \
|
||||||
--hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579
|
--hash=sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
django==3.2.14 \
|
django==3.2.15 \
|
||||||
--hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \
|
--hash=sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713 \
|
||||||
--hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56
|
--hash=sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b
|
||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# django-debug-toolbar
|
# django-debug-toolbar
|
||||||
@@ -115,9 +119,9 @@ filelock==3.7.1 \
|
|||||||
--hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \
|
--hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \
|
||||||
--hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04
|
--hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
flake8==4.0.1 \
|
flake8==5.0.4 \
|
||||||
--hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \
|
--hash=sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db \
|
||||||
--hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d
|
--hash=sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248
|
||||||
# via
|
# via
|
||||||
# -r requirements-dev.in
|
# -r requirements-dev.in
|
||||||
# flake8-docstrings
|
# flake8-docstrings
|
||||||
@@ -126,9 +130,9 @@ flake8-docstrings==1.6.0 \
|
|||||||
--hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \
|
--hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \
|
||||||
--hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b
|
--hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
identify==2.5.1 \
|
identify==2.5.3 \
|
||||||
--hash=sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa \
|
--hash=sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893 \
|
||||||
--hash=sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82
|
--hash=sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
idna==3.3 \
|
idna==3.3 \
|
||||||
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
|
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
|
||||||
@@ -140,46 +144,54 @@ isort==5.10.1 \
|
|||||||
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
|
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
|
||||||
--hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
|
--hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
mccabe==0.6.1 \
|
mccabe==0.7.0 \
|
||||||
--hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
|
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
|
||||||
--hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
|
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
|
||||||
# via flake8
|
# via flake8
|
||||||
nodeenv==1.7.0 \
|
nodeenv==1.7.0 \
|
||||||
--hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
|
--hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
|
||||||
--hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
|
--hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
pep517==0.12.0 \
|
packaging==21.3 \
|
||||||
--hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \
|
--hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
|
||||||
--hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161
|
--hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
|
||||||
# via pip-tools
|
# via build
|
||||||
pep8-naming==0.13.0 \
|
pep517==0.13.0 \
|
||||||
--hash=sha256:069ea20e97f073b3e6d4f789af2a57816f281ca64b86210c7d471117a4b6bfd0 \
|
--hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
|
||||||
--hash=sha256:9f38e6dcf867a1fb7ad47f5ff72c0ddae544a6cf64eb9f7600b7b3c0bb5980b5
|
--hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
|
||||||
|
# via build
|
||||||
|
pep8-naming==0.13.1 \
|
||||||
|
--hash=sha256:3af77cdaa9c7965f7c85a56cd579354553c9bbd3fdf3078a776f12db54dd6944 \
|
||||||
|
--hash=sha256:f7867c1a464fe769be4f972ef7b79d6df1d9aff1b1f04ecf738d471963d3ab9c
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
pip-tools==6.6.2 \
|
pip-tools==6.8.0 \
|
||||||
--hash=sha256:6b486548e5a139e30e4c4a225b3b7c2d46942a9f6d1a91143c21b1de4d02fd9b \
|
--hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \
|
||||||
--hash=sha256:f638503a9f77d98d9a7d72584b1508d3f82ed019b8fab24f4e5ad078c1b8c95e
|
--hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
platformdirs==2.5.2 \
|
platformdirs==2.5.2 \
|
||||||
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
|
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
|
||||||
--hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
|
--hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
pre-commit==2.19.0 \
|
pre-commit==2.20.0 \
|
||||||
--hash=sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10 \
|
--hash=sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7 \
|
||||||
--hash=sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615
|
--hash=sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
pycodestyle==2.8.0 \
|
pycodestyle==2.9.1 \
|
||||||
--hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \
|
--hash=sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785 \
|
||||||
--hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f
|
--hash=sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b
|
||||||
# via flake8
|
# via flake8
|
||||||
pydocstyle==6.1.1 \
|
pydocstyle==6.1.1 \
|
||||||
--hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \
|
--hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \
|
||||||
--hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4
|
--hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4
|
||||||
# via flake8-docstrings
|
# via flake8-docstrings
|
||||||
pyflakes==2.4.0 \
|
pyflakes==2.5.0 \
|
||||||
--hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \
|
--hash=sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 \
|
||||||
--hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e
|
--hash=sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3
|
||||||
# via flake8
|
# via flake8
|
||||||
|
pyparsing==3.0.9 \
|
||||||
|
--hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
|
||||||
|
--hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
|
||||||
|
# via packaging
|
||||||
pytz==2022.1 \
|
pytz==2022.1 \
|
||||||
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
|
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
|
||||||
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
|
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
|
||||||
@@ -223,18 +235,12 @@ pyyaml==6.0 \
|
|||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# pre-commit
|
# pre-commit
|
||||||
requests==2.28.0 \
|
requests==2.28.1 \
|
||||||
--hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \
|
--hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
|
||||||
--hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b
|
--hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
|
||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# coveralls
|
# coveralls
|
||||||
six==1.16.0 \
|
|
||||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
|
|
||||||
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
|
|
||||||
# via
|
|
||||||
# -c requirements.txt
|
|
||||||
# virtualenv
|
|
||||||
snowballstemmer==2.2.0 \
|
snowballstemmer==2.2.0 \
|
||||||
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
|
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
|
||||||
--hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
|
--hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
|
||||||
@@ -253,20 +259,22 @@ toml==0.10.2 \
|
|||||||
tomli==2.0.1 \
|
tomli==2.0.1 \
|
||||||
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
|
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
|
||||||
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
|
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
|
||||||
# via pep517
|
# via
|
||||||
typing-extensions==4.2.0 \
|
# build
|
||||||
--hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \
|
# pep517
|
||||||
--hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376
|
typing-extensions==4.3.0 \
|
||||||
|
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
|
||||||
|
--hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
|
||||||
# via django-test-migrations
|
# via django-test-migrations
|
||||||
urllib3==1.26.9 \
|
urllib3==1.26.11 \
|
||||||
--hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \
|
--hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \
|
||||||
--hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e
|
--hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a
|
||||||
# via
|
# via
|
||||||
# -c requirements.txt
|
# -c requirements.txt
|
||||||
# requests
|
# requests
|
||||||
virtualenv==20.15.0 \
|
virtualenv==20.16.3 \
|
||||||
--hash=sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc \
|
--hash=sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1 \
|
||||||
--hash=sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336
|
--hash=sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
wheel==0.37.1 \
|
wheel==0.37.1 \
|
||||||
--hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
|
--hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
|
||||||
|
|||||||
+12
-12
@@ -10,7 +10,7 @@ asgiref==3.5.2
|
|||||||
# via django
|
# via django
|
||||||
babel==2.10.3
|
babel==2.10.3
|
||||||
# via py-moneyed
|
# via py-moneyed
|
||||||
bleach[css]==5.0.0
|
bleach[css]==5.0.1
|
||||||
# via django-markdownify
|
# via django-markdownify
|
||||||
blessed==1.19.1
|
blessed==1.19.1
|
||||||
# via django-q
|
# via django-q
|
||||||
@@ -20,11 +20,11 @@ certifi==2022.6.15
|
|||||||
# via
|
# via
|
||||||
# requests
|
# requests
|
||||||
# sentry-sdk
|
# sentry-sdk
|
||||||
cffi==1.15.0
|
cffi==1.15.1
|
||||||
# via
|
# via
|
||||||
# cryptography
|
# cryptography
|
||||||
# weasyprint
|
# weasyprint
|
||||||
charset-normalizer==2.0.12
|
charset-normalizer==2.1.0
|
||||||
# via requests
|
# via requests
|
||||||
coreapi==2.3.3
|
coreapi==2.3.3
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
@@ -42,7 +42,7 @@ defusedxml==0.7.1
|
|||||||
# python3-openid
|
# python3-openid
|
||||||
diff-match-patch==20200713
|
diff-match-patch==20200713
|
||||||
# via django-import-export
|
# via django-import-export
|
||||||
django==3.2.14
|
django==3.2.15
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# django-allauth
|
# django-allauth
|
||||||
@@ -121,7 +121,7 @@ djangorestframework==3.13.1
|
|||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
et-xmlfile==1.1.0
|
et-xmlfile==1.1.0
|
||||||
# via openpyxl
|
# via openpyxl
|
||||||
fonttools[woff]==4.33.3
|
fonttools[woff]==4.34.4
|
||||||
# via weasyprint
|
# via weasyprint
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
@@ -135,7 +135,7 @@ itypes==1.2.0
|
|||||||
# via coreapi
|
# via coreapi
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
# via coreschema
|
# via coreschema
|
||||||
markdown==3.3.7
|
markdown==3.4.1
|
||||||
# via django-markdownify
|
# via django-markdownify
|
||||||
markuppy==1.14
|
markuppy==1.14
|
||||||
# via tablib
|
# via tablib
|
||||||
@@ -149,7 +149,7 @@ openpyxl==3.0.10
|
|||||||
# via tablib
|
# via tablib
|
||||||
pdf2image==1.16.0
|
pdf2image==1.16.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pillow==9.1.1
|
pillow==9.2.0
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# django-stdimage
|
# django-stdimage
|
||||||
@@ -194,14 +194,14 @@ redis==3.5.3
|
|||||||
# via
|
# via
|
||||||
# django-q
|
# django-q
|
||||||
# django-redis
|
# django-redis
|
||||||
requests==2.28.0
|
requests==2.28.1
|
||||||
# via
|
# via
|
||||||
# coreapi
|
# coreapi
|
||||||
# django-allauth
|
# django-allauth
|
||||||
# requests-oauthlib
|
# requests-oauthlib
|
||||||
requests-oauthlib==1.3.1
|
requests-oauthlib==1.3.1
|
||||||
# via django-allauth
|
# via django-allauth
|
||||||
sentry-sdk==1.6.0
|
sentry-sdk==1.9.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via
|
# via
|
||||||
@@ -224,13 +224,13 @@ tinycss2==1.1.1
|
|||||||
# weasyprint
|
# weasyprint
|
||||||
uritemplate==4.1.1
|
uritemplate==4.1.1
|
||||||
# via coreapi
|
# via coreapi
|
||||||
urllib3==1.26.9
|
urllib3==1.26.11
|
||||||
# via
|
# via
|
||||||
# requests
|
# requests
|
||||||
# sentry-sdk
|
# sentry-sdk
|
||||||
wcwidth==0.2.5
|
wcwidth==0.2.5
|
||||||
# via blessed
|
# via blessed
|
||||||
weasyprint==55.0
|
weasyprint==56.1
|
||||||
# via django-weasyprint
|
# via django-weasyprint
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
# via
|
# via
|
||||||
@@ -242,7 +242,7 @@ xlrd==2.0.1
|
|||||||
# via tablib
|
# via tablib
|
||||||
xlwt==1.3.0
|
xlwt==1.3.0
|
||||||
# via tablib
|
# via tablib
|
||||||
zipp==3.8.0
|
zipp==3.8.1
|
||||||
# via importlib-metadata
|
# via importlib-metadata
|
||||||
zopfli==0.2.1
|
zopfli==0.2.1
|
||||||
# via fonttools
|
# via fonttools
|
||||||
|
|||||||
Reference in New Issue
Block a user