Compare commits

...

45 Commits

Author SHA1 Message Date
Oliver
942bc5350d Bump version to 0.12.6 (#5465)
- Skipping 0.12.5 due to an error on the last release
2023-08-23 12:22:32 +10:00
github-actions[bot]
7876676114 Fix plugin pickeling (#5412) (#5457)
(cherry picked from commit 1fe382e318)

Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com>
2023-08-17 21:04:23 +10:00
Oliver
ea039645c3 Update unit tests (#5446)
- Remove failing test which no longer applies
2023-08-14 16:39:00 +10:00
github-actions[bot]
b5c7cf0779 Fix html tag in template (#5445) (#5448)
- Ensure <td> tag is closed correctly

(cherry picked from commit e7b5b145bf)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 16:28:58 +10:00
github-actions[bot]
89d8e47bab Disable "add rate" button in Admin interface (#5444) (#5447)
- Does not work with custom backend
- Throws error if the button is pressed
- So, remove the button

(cherry picked from commit a2f614ad41)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 16:19:13 +10:00
github-actions[bot]
b8e726d8a4 Catch IndexError when importing data (#5439) (#5443)
* Catch IndexError when importing data

* Also handle TypeError

(cherry picked from commit 93e4dadb49)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 15:39:07 +10:00
github-actions[bot]
3b238fdbba Fix for potential NoReverseMatch error (#5440) (#5442)
- Check that the database model really does exist in the template code

(cherry picked from commit a8118ed406)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 15:21:29 +10:00
github-actions[bot]
df8c2692a0 Fix build output unallocate button (#5426) (#5427)
(cherry picked from commit dce565b4a3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-11 10:13:52 +10:00
Oliver
7391f33a97 Do not enforce units for part parameters (#5423)
Backport of #5160
2023-08-10 21:13:46 +10:00
Oliver
b1158f7083 Bump version number to 0.12.5 (#5424) 2023-08-10 21:13:37 +10:00
github-actions[bot]
4969628150 Purchase history graph fix (#5421) (#5422)
* Fix debug messages

* Fix bug in purchase history chart

- Use new pack_quantity_native attribute

(cherry picked from commit 86ca0b27a4)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-10 14:33:37 +10:00
Oliver
57eada1da1 backport email fix (#5409)
- Backport of https://github.com/inventree/InvenTree/pull/5396
2023-08-08 15:19:00 +10:00
Oliver
f526dcdeec fix cli on 22.04 (#5204) (#5395)
* fix cli on 22.04 (#5204)

(cherry picked from commit d4fad4f5c8)

* Update weasyprint docs link

* Another link fix

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-08-03 16:21:53 +10:00
github-actions[bot]
aacf35ed47 Improve sorting of part column for BOM table (#5386) (#5387)
(cherry picked from commit c39ae80a13)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-02 17:43:08 +10:00
github-actions[bot]
ca986cba01 Fix auto-allocation of build outputs (#5378) (#5379)
- Creation of BuildItem objects was using old model references

(cherry picked from commit 668dab4175)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-01 11:26:10 +10:00
github-actions[bot]
699fb83dd4 Fix SSO check comparing id against name and extend log output (#5340) (#5377)
* add error log on SSO check failure

* sso_check_provider: fix by comparing against id

the name is the pretty printed version which not necessarily is the same
as the provider id it is compared against. This fails e.g. for the
microsoft allauth extension where the id is microsoft, but the name is
"Microsoft Graph".

Closes: #5330
(cherry picked from commit ee5416719f)

Co-authored-by: Hendrik v. Raven <hendrik@consetetur.de>
2023-08-01 10:39:46 +10:00
Oliver
dd6e225cda Update version.py (#5374)
Bump version number to 0.12.4
2023-07-31 12:45:49 +10:00
github-actions[bot]
1f3a49b1ae Fix for migration - updating from old version (#5372) (#5373)
(cherry picked from commit 90383ccb53)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-31 12:43:20 +10:00
github-actions[bot]
385e7cb478 Return 404 on API requests other than GET (#5365) (#5366)
- Other request methods need love too!

(cherry picked from commit 59ffdcaa19)
(cherry picked from commit b89a120f9e)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 22:14:55 +10:00
github-actions[bot]
73768bfee1 Handle purchase price export for .xls files (#5362) (#5363)
(cherry picked from commit 87da286f2f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 15:36:34 +10:00
github-actions[bot]
946fe2df29 Handle errors when printing reports (#5360) (#5361)
- Re-throw as a ValidationError
- Results in a 400 error, not a 500

(cherry picked from commit 5f3d3b28b3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 14:23:43 +10:00
github-actions[bot]
afa7ed873f Exclude some common fields from django-import-export (#5349) (#5351)
- Add "get_fields()" method to InvenTreeResource
- Override default behaviour and exclude some common fields
- Will flow down to any inheriting classes

(cherry picked from commit 941451203a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-26 17:22:21 +10:00
github-actions[bot]
46da332afe Allow duplicate BOM items when duplicating a part (#5347) (#5350)
(cherry picked from commit 6660508326)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-26 16:54:20 +10:00
Oliver
072b7b3146 Update version.py
Bump version number to 0.12.3
2023-07-25 11:46:41 +10:00
github-actions[bot]
1d51b2a058 Email config fix (#5336) (#5338)
* Change for DEFAULT_FROM_EMAIL

- Use USERNAME if not specified

(cherry picked from commit 487ac917c90e9fe3da4effaa9326b707ceecd321)

* Email configuration fails if DEFAULT_FROM_EMAIL not set

(cherry picked from commit 01e573c3a2702e7c21ed13b0cb44280c89d3dee1)

* Docs update

(cherry picked from commit bfedb9cf87)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-25 11:45:17 +10:00
github-actions[bot]
08f9bebdf0 Fix admin url to point to right model (#5319) (#5321)
(cherry picked from commit 9b377ccfbf)

Co-authored-by: Marcel Pörner <me@nerade.de>
2023-07-23 22:38:27 +10:00
github-actions[bot]
6d6629f11c Stock installed table fix (#5305) (#5306)
* Prevent installed items from being hidden

* Fix parent / child relationship

(cherry picked from commit f70294b247)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-21 23:57:00 +10:00
github-actions[bot]
db88fbda11 Fix company index page title (#5288) (#5291)
(cherry picked from commit 3baa640d70)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-20 10:07:39 +10:00
github-actions[bot]
49c9b5b1aa Docker build: Update python deps (#5270) (#5271)
* Update python deps

* Update requirements.in

* Fix requirements-dev.txt

(cherry picked from commit b717011f06)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-18 20:11:29 +10:00
github-actions[bot]
e1a0e79ead Fix settings function callback (#5259) (#5262)
* fix settings function callback

* merge instance filters and passed keys

(cherry picked from commit df77305d60)

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-07-17 20:23:44 +10:00
github-actions[bot]
ab22f2a04d Fix language code for pt-br (#5256) (#5257)
- Has to be lowercase in settings.py to work correctly

(cherry picked from commit 20b59c3575)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-16 19:37:27 +10:00
github-actions[bot]
8a58bf5ffa Only update theme if value provided (#5240) (#5241)
- Handles case where null or invalid value provided

(cherry picked from commit 41167f22c9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-13 20:39:28 +10:00
Oliver
6730098bac Update version.py (#5238)
Bump version number to 0.12.2
2023-07-13 15:13:47 +10:00
github-actions[bot]
93b44ad8e6 fix typo (#5236) (#5237)
(cherry picked from commit bd1689095d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-13 11:03:28 +10:00
github-actions[bot]
9b5e828b87 Protected settings fix (#5229) (#5231)
* Hide protected setting in settings view

* Implement custom serializer for setting value

- Return '***' if the setting is protected

* Implement to_internal_value

* Stringify

* Add protected setting to sample plugin

* Unit tests for plugin settings API

* Update unit test

(cherry picked from commit 01f2aa5f74)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-12 16:31:28 +10:00
github-actions[bot]
cf5d637678 Add missing callback for attachment delete button (#5219) (#5220)
(cherry picked from commit b3dcc28bd9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-11 11:30:57 +10:00
github-actions[bot]
feb2acf668 Fix link to SalesOrder in stock history table (#5210) (#5211)
(cherry picked from commit 8fb7612894)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-10 13:23:09 +10:00
Oliver
0017570dd3 Bump version number to 0.12.1 (#5201) 2023-07-07 14:25:30 +10:00
github-actions[bot]
4c41a50bb1 Fix allocation check for completing build order (#5199) (#5200)
- Allocation check only applies to untracked line items

(cherry picked from commit 1f81daadf6)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-07 13:48:18 +10:00
github-actions[bot]
eab3fdcf2c Fix quantity aggregation for stock table (#5188) (#5190)
* Fix quantity aggregation for stock table

- Stock quantity can only be added together if units are the same

* Add stock total footer to part table

(cherry picked from commit 773dd3b210)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-06 12:55:22 +10:00
github-actions[bot]
c59eee7359 Param fix (#5183) (#5184)
* Handle AttributeError in convert_physical_value

* Added new unit test

(cherry picked from commit 9abcc0ec34)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-06 11:11:27 +10:00
github-actions[bot]
4a5ebf8f01 Handle exception when creating default labels (#5163) (#5166)
* Handle exception when creating default labels

- Running workers in parallel may cause race conditions
- Catch any exception which is raised

* Prevent password from being logged

* Update default timeout for docker

(cherry picked from commit 8b730884d7)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-04 22:54:21 +10:00
github-actions[bot]
698798fee7 Order table improvements (#5151) (#5152)
- prevent "double loading" of order tables

(cherry picked from commit 17c2070503)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-04 16:23:43 +10:00
github-actions[bot]
2660889879 Rendering fix for build allocation table (#5145) (#5149)
- Fix link to part
- Fix link to stock item

(cherry picked from commit 5f61b5f120)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-04 13:40:40 +10:00
github-actions[bot]
01aaf95a0e fix: add missing build model property (#5127) (#5132)
* fix: add missing virtual build property

* chore: improve docstring

(cherry picked from commit 2e7c86ff92)

Co-authored-by: Mark Oude Elberink <mark@oude-elberink.de>
2023-07-03 10:04:02 +10:00
49 changed files with 523 additions and 186 deletions

View File

@@ -1,5 +1,10 @@
"""Admin classes"""
from django.contrib import admin
from django.http.request import HttpRequest
from djmoney.contrib.exchange.admin import RateAdmin
from djmoney.contrib.exchange.models import Rate
from import_export.resources import ModelResource
@@ -31,3 +36,27 @@ class InvenTreeResource(ModelResource):
row[idx] = val
return row
def get_fields(self, **kwargs):
"""Return fields, with some common exclusions"""
fields = super().get_fields(**kwargs)
fields_to_exclude = [
'metadata',
'lft', 'rght', 'tree_id', 'level',
]
return [f for f in fields if f.column_name not in fields_to_exclude]
class CustomRateAdmin(RateAdmin):
"""Admin interface for the Rate class"""
def has_add_permission(self, request: HttpRequest) -> bool:
"""Disable the 'add' permission for Rate objects"""
return False
admin.site.unregister(Rate)
admin.site.register(Rate, CustomRateAdmin)

View File

@@ -59,14 +59,39 @@ class NotFoundView(AjaxView):
permission_classes = [permissions.AllowAny]
def get(self, request, *args, **kwargs):
"""Process an `not found` event on the API."""
data = {
'details': _('API endpoint not found'),
'url': request.build_absolute_uri(),
}
def not_found(self, request):
"""Return a 404 error"""
return JsonResponse(
{
'detail': _('API endpoint not found'),
'url': request.build_absolute_uri(),
},
status=404
)
return JsonResponse(data, status=404)
def options(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def get(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def post(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def patch(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def put(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def delete(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
class BulkDeleteMixin:

View File

@@ -195,8 +195,8 @@ class InvenTreeConfig(AppConfig):
else:
new_user = user.objects.create_superuser(add_user, add_email, add_password)
logger.info(f'User {str(new_user)} was created!')
except IntegrityError as _e:
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
except IntegrityError:
logger.warning(f'The user "{add_user}" could not be created')
# do not try again
settings.USER_ADDED = True

View File

@@ -91,7 +91,7 @@ def convert_physical_value(value: str, unit: str = None):
# At this point we *should* have a valid pint value
# To double check, look at the maginitude
float(val.magnitude)
except (TypeError, ValueError):
except (TypeError, ValueError, AttributeError):
error = _('Provided value is not a valid number')
except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError):
error = _('Provided value has an invalid unit')

View File

@@ -17,6 +17,7 @@ def is_email_configured():
NOTE: This does not check if the configuration is valid!
"""
configured = True
testing = settings.TESTING
if InvenTree.ready.isInTestMode():
return False
@@ -28,17 +29,24 @@ def is_email_configured():
configured = False
# Display warning unless in test mode
if not settings.TESTING: # pragma: no cover
if not testing: # pragma: no cover
logger.debug("EMAIL_HOST is not configured")
# Display warning unless in test mode
if not settings.EMAIL_HOST_USER and not settings.TESTING: # pragma: no cover
if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover
logger.debug("EMAIL_HOST_USER is not configured")
# Display warning unless in test mode
if not settings.EMAIL_HOST_PASSWORD and not settings.TESTING: # pragma: no cover
if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover
logger.debug("EMAIL_HOST_PASSWORD is not configured")
# Email sender must be configured
if not settings.DEFAULT_FROM_EMAIL:
configured = False
if not testing: # pragma: no cover
logger.warning("DEFAULT_FROM_EMAIL is not configured")
return configured

View File

@@ -292,6 +292,15 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, Default
return False
def get_email_confirmation_url(self, request, emailconfirmation):
"""Construct the email confirmation url"""
from InvenTree.helpers_model import construct_absolute_url
url = super().get_email_confirmation_url(request, emailconfirmation)
url = construct_absolute_url(url)
return url
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
"""Override of adapter to use dynamic settings."""

View File

@@ -50,9 +50,7 @@ def construct_absolute_url(*arg, **kwargs):
# Otherwise, try to use the InvenTree setting
try:
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
except ProgrammingError:
pass
except OperationalError:
except (ProgrammingError, OperationalError):
pass
if not site_url:

View File

@@ -601,6 +601,8 @@ DATABASES = {
REMOTE_LOGIN = get_boolean_setting('INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False)
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER')
LOGIN_REDIRECT_URL = "/index/"
# sentry.io integration for error reporting
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
@@ -757,14 +759,14 @@ LANGUAGES = [
('no', _('Norwegian')),
('pl', _('Polish')),
('pt', _('Portuguese')),
('pt-BR', _('Portuguese (Brazilian)')),
('pt-br', _('Portuguese (Brazilian)')),
('ru', _('Russian')),
('sl', _('Slovenian')),
('sv', _('Swedish')),
('th', _('Thai')),
('tr', _('Turkish')),
('vi', _('Vietnamese')),
('zh-hans', _('Chinese')),
('zh-hans', _('Chinese (Simplified)')),
]
# Testing interface translations
@@ -822,6 +824,10 @@ EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
# If "from" email not specified, default to the username
if not DEFAULT_FROM_EMAIL:
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
EMAIL_USE_LOCALTIME = False
EMAIL_TIMEOUT = 60

View File

@@ -56,6 +56,23 @@ class ConversionTest(TestCase):
q = InvenTree.conversion.convert_physical_value(val).to_base_units()
self.assertEqual(q.magnitude, expected)
def test_invalid_values(self):
"""Test conversion of invalid inputs"""
inputs = [
'-',
';;',
'-x',
'?',
'--',
'+',
'++',
]
for val in inputs:
with self.assertRaises(ValidationError):
InvenTree.conversion.convert_physical_value(val)
class ValidatorTest(TestCase):
"""Simple tests for custom field validators."""

View File

@@ -9,7 +9,8 @@ from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic.base import RedirectView
from dj_rest_auth.registration.views import (SocialAccountDisconnectView,
from dj_rest_auth.registration.views import (ConfirmEmailView,
SocialAccountDisconnectView,
SocialAccountListView)
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
@@ -74,13 +75,16 @@ apipatterns = [
# InvenTree information endpoint
path('', InfoView.as_view(), name='api-inventree-info'),
# Third party API endpoints
path('auth/', include('dj_rest_auth.urls')),
path('auth/registration/', include('dj_rest_auth.registration.urls')),
path('auth/providers/', SocialProvierListView.as_view(), name='social_providers'),
path('auth/social/', include(social_auth_urlpatterns)),
path('auth/social/', SocialAccountListView.as_view(), name='social_account_list'),
path('auth/social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
# Auth API endpoints
path('auth/', include([
re_path(r'^registration/account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'),
path('registration/', include('dj_rest_auth.registration.urls')),
path('providers/', SocialProvierListView.as_view(), name='social_providers'),
path('social/', include(social_auth_urlpatterns)),
path('social/', SocialAccountListView.as_view(), name='social_account_list'),
path('social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
path('', include('dj_rest_auth.urls')),
])),
# Unknown endpoint
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),

View File

@@ -18,7 +18,7 @@ from dulwich.repo import NotGitRepository, Repo
from .api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.12.0"
INVENTREE_SW_VERSION = "0.12.6"
# Discover git
try:

View File

@@ -640,8 +640,12 @@ class AppearanceSelectView(RedirectView):
user_theme = common_models.ColorTheme()
user_theme.user = request.user
user_theme.name = theme
user_theme.save()
if theme:
try:
user_theme.name = theme
user_theme.save()
except Exception:
pass
return redirect(reverse_lazy('settings'))

View File

@@ -361,6 +361,11 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
return self.build_lines.filter(bom_item__sub_part__trackable=False)
@property
def are_untracked_parts_allocated(self):
"""Returns True if all untracked parts are allocated for this BuildOrder."""
return self.is_fully_allocated(tracked=False)
def has_untracked_line_items(self):
"""Returns True if this BuildOrder has non trackable BomItems."""
return self.has_untracked_line_items.count() > 0
@@ -714,14 +719,22 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
if items.exists() and items.count() == 1:
stock_item = items[0]
# Allocate the stock item
BuildItem.objects.create(
build=self,
bom_item=bom_item,
stock_item=stock_item,
quantity=1,
install_into=output,
)
# Find the 'BuildLine' object which points to this BomItem
try:
build_line = BuildLine.objects.get(
build=self,
bom_item=bom_item
)
# Allocate the stock items against the BuildLine
BuildItem.objects.create(
build_line=build_line,
stock_item=stock_item,
quantity=1,
install_into=output,
)
except BuildLine.DoesNotExist:
pass
else:
"""Create a single build output of the given quantity."""

View File

@@ -630,7 +630,7 @@ class BuildCompleteSerializer(serializers.Serializer):
return {
'overallocated': build.is_overallocated(),
'allocated': build.is_fully_allocated(),
'allocated': build.are_untracked_parts_allocated,
'remaining': build.remaining,
'incomplete': build.incomplete_count,
}
@@ -663,7 +663,7 @@ class BuildCompleteSerializer(serializers.Serializer):
"""Check if the 'accept_unallocated' field is required"""
build = self.context['build']
if not build.is_fully_allocated() and not value:
if not build.are_untracked_parts_allocated and not value:
raise ValidationError(_('Required stock has not been fully allocated'))
return value

View File

@@ -190,7 +190,7 @@ class BaseInvenTreeSetting(models.Model):
kwargs: Keyword arguments to pass to the function
"""
# Get action
setting = self.get_setting_definition(self.key, *args, **kwargs)
setting = self.get_setting_definition(self.key, *args, **{**self.get_filters_for_instance(), **kwargs})
settings_fnc = setting.get(reference, None)
# Execute if callable

View File

@@ -13,6 +13,25 @@ from InvenTree.serializers import (InvenTreeImageSerializerField,
InvenTreeModelSerializer)
class SettingsValueField(serializers.Field):
"""Custom serializer field for a settings value."""
def get_attribute(self, instance):
"""Return the object instance, not the attribute value."""
return instance
def to_representation(self, instance):
"""Return the value of the setting:
- Protected settings are returned as '***'
"""
return '***' if instance.protected else str(instance.value)
def to_internal_value(self, data):
"""Return the internal value of the setting"""
return str(data)
class SettingsSerializer(InvenTreeModelSerializer):
"""Base serializer for a settings object."""
@@ -30,6 +49,8 @@ class SettingsSerializer(InvenTreeModelSerializer):
api_url = serializers.CharField(read_only=True)
value = SettingsValueField()
def get_choices(self, obj):
"""Returns the choices available for a given item."""
results = []
@@ -45,16 +66,6 @@ class SettingsSerializer(InvenTreeModelSerializer):
return results
def get_value(self, obj):
"""Make sure protected values are not returned."""
# never return protected values
if obj.protected:
result = '***'
else:
result = obj.value
return result
class GlobalSettingsSerializer(SettingsSerializer):
"""Serializer for the InvenTreeSetting model."""

View File

@@ -5,7 +5,7 @@
{% load inventree_extras %}
{% block page_title %}
{% inventree_title %} | {% trans "Supplier List" %}
{% inventree_title %}{% if title %} | {{ title }}{% endif %}
{% endblock page_title %}
{% block heading %}

View File

@@ -27,7 +27,7 @@
{% block actions %}
{% if user.is_staff and perms.company.change_company %}
{% url 'admin:company_supplierpart_change' part.pk as url %}
{% url 'admin:company_manufacturerpart_change' part.pk as url %}
{% include "admin_button.html" with url=url %}
{% endif %}
{% if roles.purchase_order.change %}

View File

@@ -182,13 +182,15 @@ class LabelConfig(AppConfig):
logger.info(f"Creating entry for {model} '{label['name']}'")
model.objects.create(
name=label['name'],
description=label['description'],
label=filename,
filters='',
enabled=True,
width=label['width'],
height=label['height'],
)
return
try:
model.objects.create(
name=label['name'],
description=label['description'],
label=filename,
filters='',
enabled=True,
width=label['width'],
height=label['height'],
)
except Exception:
logger.warning(f"Failed to create label '{label['name']}'")

View File

@@ -171,6 +171,14 @@ class PurchaseOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
SKU = Field(attribute='part__SKU', readonly=True)
def dehydrate_purchase_price(self, line):
"""Return a string value of the 'purchase_price' field, rather than the 'Money' object"""
if line.purchase_price:
return line.purchase_price.amount
else:
return ''
class PurchaseOrderExtraLineResource(PriceResourceMixin, InvenTreeResource):
"""Class for managing import / export of PurchaseOrderExtraLine data."""

View File

@@ -2031,10 +2031,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
if bom_item.part in my_ancestors and bom_item.inherited:
continue
# Skip if already exists
if BomItem.objects.filter(part=self, sub_part=bom_item.sub_part).exists():
continue
# Skip (or throw error) if BomItem is not valid
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
continue
@@ -3532,15 +3528,6 @@ class PartParameter(MetadataMixin, models.Model):
super().clean()
# Validate the parameter data against the template units
if self.template.units:
try:
InvenTree.conversion.convert_physical_value(self.data, self.template.units)
except ValidationError as e:
raise ValidationError({
'data': e.message
})
# Validate the parameter data against the template choices
if choices := self.template.get_choices():
if self.data not in choices:

View File

@@ -38,9 +38,13 @@ def sso_check_provider(provider):
from allauth.socialaccount.models import SocialApp
# First, check that the provider is enabled
apps = SocialApp.objects.filter(provider__iexact=provider.name)
apps = SocialApp.objects.filter(provider__iexact=provider.id)
if not apps.exists():
logging.error(
"SSO SocialApp %s does not exist (known providers: %s)",
provider.id, [obj.provider for obj in SocialApp.objects.all()]
)
return False
# Next, check that the provider is correctly configured

View File

@@ -168,12 +168,6 @@ class ParameterTests(TestCase):
param = PartParameter(part=prt, template=template, data=value)
param.full_clean()
# Test that invalid parameters fail
for value in ['3 Amps', '-3 zogs', '3.14F']:
param = PartParameter(part=prt, template=template, data=value)
with self.assertRaises(django_exceptions.ValidationError):
param.full_clean()
def test_param_unit_conversion(self):
"""Test that parameters are correctly converted to template units"""

View File

@@ -168,7 +168,11 @@ class PartImport(FileManagementFormView):
for row in self.rows:
# check each submitted column
for idx in col_ids:
data = row['data'][col_ids[idx]]['cell']
try:
data = row['data'][col_ids[idx]]['cell']
except (IndexError, TypeError):
continue
if idx in self.file_manager.OPTIONAL_MATCH_HEADERS:
try:

View File

@@ -103,6 +103,12 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
# Save plugin
self.plugin: InvenTreePlugin = plugin
def __getstate__(self):
"""Customize pickeling behaviour."""
state = super().__getstate__()
state.pop("plugin", None) # plugin cannot be pickelt in some circumstances when used with drf views, remove it (#5408)
return state
def save(self, force_insert=False, force_update=False, *args, **kwargs):
"""Extend save method to reload plugins if the 'active' status changes."""
reload = kwargs.pop('no_reload', False) # check if no_reload flag is set

View File

@@ -72,6 +72,12 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
'description': 'Select a part object from the database',
'model': 'part.part',
},
'PROTECTED_SETTING': {
'name': 'Protected Setting',
'description': 'A protected setting, hidden from the UI',
'default': 'ABC-123',
'protected': True,
}
}
NAVIGATION = [

View File

@@ -193,3 +193,76 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
with self.assertRaises(NotFound) as exc:
check_plugin(plugin_slug=None, plugin_pk='123')
self.assertEqual(str(exc.exception.detail), "Plugin '123' not installed")
def test_plugin_settings(self):
"""Test plugin settings access via the API"""
# Ensure we have superuser permissions
self.user.is_superuser = True
self.user.save()
# Activate the 'sample' plugin via the API
cfg = PluginConfig.objects.filter(key='sample').first()
url = reverse('api-plugin-detail-activate', kwargs={'pk': cfg.pk})
self.client.patch(url, {}, expected_code=200)
# Valid plugin settings endpoints
valid_settings = [
'SELECT_PART',
'API_KEY',
'NUMERICAL_SETTING',
]
for key in valid_settings:
response = self.get(
reverse('api-plugin-setting-detail', kwargs={
'plugin': 'sample',
'key': key
}))
self.assertEqual(response.data['key'], key)
# Test that an invalid setting key raises a 404 error
response = self.get(
reverse('api-plugin-setting-detail', kwargs={
'plugin': 'sample',
'key': 'INVALID_SETTING'
}),
expected_code=404
)
# Test that a protected setting returns hidden value
response = self.get(
reverse('api-plugin-setting-detail', kwargs={
'plugin': 'sample',
'key': 'PROTECTED_SETTING'
}),
expected_code=200
)
self.assertEqual(response.data['value'], '***')
# Test that we can update a setting value
response = self.patch(
reverse('api-plugin-setting-detail', kwargs={
'plugin': 'sample',
'key': 'NUMERICAL_SETTING'
}),
{
'value': 456
},
expected_code=200
)
self.assertEqual(response.data['value'], '456')
# Retrieve the value again
response = self.get(
reverse('api-plugin-setting-detail', kwargs={
'plugin': 'sample',
'key': 'NUMERICAL_SETTING'
}),
expected_code=200
)
self.assertEqual(response.data['value'], '456')

View File

@@ -18,6 +18,7 @@ import InvenTree.helpers
import order.models
import part.models
from InvenTree.api import MetadataView
from InvenTree.exceptions import log_error
from InvenTree.filters import InvenTreeSearchFilter
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
from stock.models import StockItem, StockItemAttachment
@@ -181,78 +182,90 @@ class ReportPrintMixin:
# Start with a default report name
report_name = "report.pdf"
# Merge one or more PDF files into a single download
for item in items_to_print:
report = self.get_object()
report.object_to_print = item
try:
# Merge one or more PDF files into a single download
for item in items_to_print:
report = self.get_object()
report.object_to_print = item
report_name = report.generate_filename(request)
output = report.render(request)
report_name = report.generate_filename(request)
output = report.render(request)
# Run report callback for each generated report
self.report_callback(item, output, request)
# Run report callback for each generated report
self.report_callback(item, output, request)
try:
if debug_mode:
outputs.append(report.render_as_string(request))
else:
outputs.append(output)
except TemplateDoesNotExist as e:
template = str(e)
if not template:
template = report.template
try:
if debug_mode:
outputs.append(report.render_as_string(request))
else:
outputs.append(output)
except TemplateDoesNotExist as e:
template = str(e)
if not template:
template = report.template
return Response(
{
'error': _(f"Template file '{template}' is missing or does not exist"),
},
status=400,
return Response(
{
'error': _(f"Template file '{template}' is missing or does not exist"),
},
status=400,
)
if not report_name.endswith('.pdf'):
report_name += '.pdf'
if debug_mode:
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
html = "\n".join(outputs)
return HttpResponse(html)
else:
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
pages = []
try:
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
except TemplateDoesNotExist as e:
template = str(e)
if not template:
template = report.template
return Response(
{
'error': _(f"Template file '{template}' is missing or does not exist"),
},
status=400,
)
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user, cache=False)
return InvenTree.helpers.DownloadFile(
pdf,
report_name,
content_type='application/pdf',
inline=inline,
)
if not report_name.endswith('.pdf'):
report_name += '.pdf'
except Exception as exc:
# Log the exception to the database
log_error(request.path)
if debug_mode:
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
html = "\n".join(outputs)
return HttpResponse(html)
else:
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
pages = []
try:
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
except TemplateDoesNotExist as e:
template = str(e)
if not template:
template = report.template
return Response(
{
'error': _(f"Template file '{template}' is missing or does not exist"),
},
status=400,
)
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user, cache=False)
return InvenTree.helpers.DownloadFile(
pdf,
report_name,
content_type='application/pdf',
inline=inline,
)
# Re-throw the exception to the client as a DRF exception
raise ValidationError({
'error': 'Report printing failed',
'detail': str(exc),
'path': request.path,
})
def get(self, request, *args, **kwargs):
"""Default implementation of GET for a print endpoint.

View File

@@ -69,6 +69,7 @@ def fix_purchase_price(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('company', '0047_supplierpart_pack_size'),
('stock', '0093_auto_20230217_2140'),
]

View File

@@ -368,7 +368,12 @@
<tr>
<td><span class='fas fa-th-list'></span></td>
<td>{% trans "Sales Order" %}</td>
<td><a href="{% url 'so-detail' item.sales_order.id %}">{{ item.sales_order.reference }}</a> - <a href="{% url 'company-detail' item.sales_order.customer.id %}">{{ item.sales_order.customer.name }}</a></td>
<td>
<a href="{% url 'so-detail' item.sales_order.id %}">{{ item.sales_order.reference }}</a>
{% if item.sales_order.customer %}
- <a href="{% url 'company-detail' item.sales_order.customer.id %}">{{ item.sales_order.customer.name }}</a>
{% endif %}
</td>
</tr>
{% else %}
{% if allocated_to_sales_orders %}

View File

@@ -22,7 +22,9 @@
{{ setting.description }}
</td>
<td>
{% if setting.is_bool %}
{% if setting.protected %}
<span style='color: red;'>***</span> <span class='fas fa-lock icon-red'></span>
{% elif setting.is_bool %}
{% include "InvenTree/settings/setting_boolean.html" %}
{% else %}
<div id='setting-{{ setting.pk }}'>

View File

@@ -281,10 +281,20 @@ function loadAttachmentTable(url, options) {
sidePagination: 'server',
onPostBody: function() {
// Add callback for 'delete' button
if (permissions.delete) {
$(table).find('.button-attachment-delete').click(function() {
let pk = $(this).attr('pk');
let attachments = $(table).bootstrapTable('getRowByUniqueId', pk);
deleteAttachments([attachments], url, options);
});
}
// Add callback for 'edit' button
if (permissions.change) {
$(table).find('.button-attachment-edit').click(function() {
var pk = $(this).attr('pk');
let pk = $(this).attr('pk');
constructForm(`${url}${pk}/`, {
fields: {

View File

@@ -905,6 +905,18 @@ function loadBomTable(table, options={}) {
title: '{% trans "Part" %}',
sortable: true,
switchable: false,
sorter: function(_valA, _valB, rowA, rowB) {
let name_a = rowA.sub_part_detail.full_name;
let name_b = rowB.sub_part_detail.full_name;
if (name_a > name_b) {
return 1;
} else if (name_a < name_b) {
return -1;
} else {
return 0;
}
},
formatter: function(value, row) {
var url = `/part/${row.sub_part}/`;
var html = '';

View File

@@ -2173,9 +2173,6 @@ function loadBuildTable(table, options) {
customView: function(data) {
return `<div id='build-order-calendar'></div>`;
},
onRefresh: function() {
loadBuildTable(table, options);
},
onLoadSuccess: function() {
if (tree_enable) {
@@ -2255,16 +2252,16 @@ function renderBuildLineAllocationTable(element, build_line, options={}) {
{
field: 'part',
title: '{% trans "Part" %}',
formatter: function(value, row) {
formatter: function(_value, row) {
let html = imageHoverIcon(row.part_detail.thumbnail);
html += renderLink(row.part_detail.full_name, `/part/${value}/`);
html += renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
return html;
}
},
{
field: 'quantity',
title: '{% trans "Allocated Quantity" %}',
formatter: function(value, row) {
formatter: function(_value, row) {
let text = '';
let url = '';
let serial = row.serial;
@@ -2294,8 +2291,8 @@ function renderBuildLineAllocationTable(element, build_line, options={}) {
title: '{% trans "Location" %}',
formatter: function(value, row) {
if (row.location_detail) {
var text = shortenString(row.location_detail.pathstring);
var url = `/stock/location/${row.location}/`;
let text = shortenString(row.location_detail.pathstring);
let url = `/stock/location/${row.location_detail.pk}/`;
return renderLink(text, url);
} else {
@@ -2660,6 +2657,7 @@ function loadBuildLineTable(table, build_id, options={}) {
deallocateStock(build_id, {
build_line: pk,
output: output,
onSuccess: function() {
$(table).bootstrapTable('refresh');
}

View File

@@ -1404,6 +1404,7 @@ function createPartParameter(part_id, options={}) {
function editPartParameter(param_id, options={}) {
options.fields = partParameterFields();
options.title = '{% trans "Edit Parameter" %}';
options.focus = 'data';
options.processBeforeUpload = function(data) {
// Convert data to string
@@ -2367,6 +2368,38 @@ function loadPartTable(table, url, options={}) {
});
return text;
},
footerFormatter: function(data) {
// Display "total" stock quantity of all rendered rows
// Requires that all parts have the same base units!
let total = 0;
let units = new Set();
data.forEach(function(row) {
units.add(row.units || null);
if (row.total_in_stock != null) {
total += row.total_in_stock;
}
});
if (data.length == 0) {
return '-';
} else if (units.size > 1) {
return '-';
} else {
let output = `${total}`;
if (units.size == 1) {
let unit = units.values().next().value;
if (unit) {
output += ` [${unit}]`;
}
}
return output;
}
}
});
@@ -2442,6 +2475,7 @@ function loadPartTable(table, url, options={}) {
showColumns: true,
showCustomView: grid_view,
showCustomViewButton: false,
showFooter: true,
onPostBody: function() {
grid_view = inventreeLoad('part-grid-view') == 1;
if (grid_view) {

View File

@@ -292,7 +292,7 @@ function loadBomPricingChart(options={}) {
var part = options.part;
if (!part) {
console.error('No part provided to loadPurchasePriceHistoryTable');
console.error('No part provided to loadBomPricingChart');
return;
}
@@ -434,7 +434,7 @@ function loadPartSupplierPricingTable(options={}) {
var part = options.part;
if (!part) {
console.error('No part provided to loadPurchasePriceHistoryTable');
console.error('No part provided to loadPartSupplierPricingTable');
return;
}
@@ -764,7 +764,21 @@ function loadPurchasePriceHistoryTable(options={}) {
data = data.sort((a, b) => (a.order_detail.complete_date - b.order_detail.complete_date));
var graphLabels = Array.from(data, (x) => (`${x.order_detail.reference} - ${x.order_detail.complete_date}`));
var graphValues = Array.from(data, (x) => (x.purchase_price / x.supplier_part_detail.pack_size));
var graphValues = Array.from(data, (x) => {
let pp = x.purchase_price;
let div = 1.0;
if (x.supplier_part_detail) {
div = parseFloat(x.supplier_part_detail.pack_quantity_native);
if (isNaN(div) || !isFinite(div)) {
div = 1.0;
}
}
return pp / div;
});
if (chart) {
chart.destroy();

View File

@@ -1759,9 +1759,6 @@ function loadPurchaseOrderTable(table, options) {
customView: function(data) {
return `<div id='purchase-order-calendar'></div>`;
},
onRefresh: function() {
loadPurchaseOrderTable(table, options);
},
onLoadSuccess: function() {
if (display_mode == 'calendar') {

View File

@@ -262,9 +262,6 @@ function loadReturnOrderTable(table, options={}) {
formatNoMatches: function() {
return '{% trans "No return orders found" %}';
},
onRefresh: function() {
loadReturnOrderTable(table, options);
},
onLoadSuccess: function() {
// TODO
},

View File

@@ -735,9 +735,6 @@ function loadSalesOrderTable(table, options) {
customView: function(data) {
return `<div id='purchase-order-calendar'></div>`;
},
onRefresh: function() {
loadSalesOrderTable(table, options);
},
onLoadSuccess: function() {
if (display_mode == 'calendar') {

View File

@@ -2068,13 +2068,36 @@ function loadStockTable(table, options) {
// Display "total" stock quantity of all rendered rows
let total = 0;
// Keep track of the whether all units are the same
// If different units are found, we cannot aggregate the quantities
let units = new Set();
data.forEach(function(row) {
units.add(row.part_detail.units || null);
if (row.quantity != null) {
total += row.quantity;
}
});
return total;
if (data.length == 0) {
return '-';
} else if (units.size > 1) {
return '-';
} else {
let output = `${total}`;
if (units.size == 1) {
let unit = units.values().next().value;
if (unit) {
output += ` [${unit}]`;
}
}
return output;
}
}
};
@@ -2352,6 +2375,10 @@ function loadStockTable(table, options) {
let row = table.bootstrapTable('getRowByUniqueId', stock_item);
row.installed_items_received = true;
for (let ii = 0; ii < response.length; ii++) {
response[ii].belongs_to_item = stock_item;
}
table.bootstrapTable('updateByUniqueId', stock_item, row, true);
table.bootstrapTable('append', response);
@@ -2367,6 +2394,7 @@ function loadStockTable(table, options) {
}
let parent_id = 'top-level';
let loaded = false;
table.inventreeTable({
method: 'get',
@@ -2382,13 +2410,25 @@ function loadStockTable(table, options) {
showFooter: true,
columns: columns,
treeEnable: show_installed_items,
rootParentId: parent_id,
parentIdField: 'belongs_to',
rootParentId: show_installed_items ? parent_id : null,
parentIdField: show_installed_items ? 'belongs_to_item' : null,
uniqueId: 'pk',
idField: 'pk',
treeShowField: 'part',
onPostBody: function() {
treeShowField: show_installed_items ? 'part' : null,
onLoadSuccess: function(data) {
let records = data.results || data;
// Set the 'parent' ID for each root item
if (!loaded && show_installed_items) {
for (let i = 0; i < records.length; i++) {
records[i].belongs_to_item = parent_id;
}
loaded = true;
$(table).bootstrapTable('load', records);
}
},
onPostBody: function() {
if (show_installed_items) {
table.treegrid({
treeColumn: 1,
@@ -2618,7 +2658,7 @@ function loadStockLocationTable(table, options) {
} else {
html += `
<a href='#' pk='${row.pk}' class='load-sub-location'>
<span class='fas fa-sync-alt' title='{% trans "Load Subloactions" %}'></span>
<span class='fas fa-sync-alt' title='{% trans "Load Sublocations" %}'></span>
</a> `;
}
}
@@ -2809,7 +2849,7 @@ function loadStockTrackingTable(table, options) {
if (details.salesorder_detail) {
html += renderLink(
details.salesorder_detail.reference,
`/order/sales-order/${details.salesorder}`
`/order/sales-order/${details.salesorder}/`
);
} else {
html += `<em>{% trans "Sales Order no longer exists" %}</em>`;

View File

@@ -1,3 +1,3 @@
web: env/bin/gunicorn --chdir $APP_HOME/InvenTree -c InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
worker: env/bin/python InvenTree/manage.py qcluster
cli: . env/bin/activate && exec env/bin/python -m invoke
cli: echo "" && . env/bin/activate && exec env/bin/python -m invoke

View File

@@ -42,7 +42,7 @@ INVENTREE_DB_PORT=5432
#INVENTREE_CACHE_PORT=6379
# Options for gunicorn server
INVENTREE_GUNICORN_TIMEOUT=30
INVENTREE_GUNICORN_TIMEOUT=90
# Enable custom plugins?
INVENTREE_PLUGINS_ENABLED=False

View File

@@ -2,7 +2,7 @@
# Basic package requirements
invoke>=1.4.0 # Invoke build tool
pyyaml>=6.0
pyyaml>=6.0.1
setuptools==65.6.3
wheel>=0.37.0

View File

@@ -17,7 +17,7 @@ In addition to providing the ability for end-users to provide their own reportin
InvenTree report templates utilize the powerful [WeasyPrint](https://weasyprint.org/) PDF generation engine.
!!! info "WeasyPrint"
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://weasyprint.readthedocs.io/en/stable/) for further information.
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/) for further information.
### Stylesheets

View File

@@ -155,9 +155,16 @@ The following email settings are available:
| INVENTREE_EMAIL_PASSWORD | email.password | Email account password | *Not specified* |
| INVENTREE_EMAIL_TLS | email.tls | Enable TLS support | False |
| INVENTREE_EMAIL_SSL | email.ssl | Enable SSL support | False |
| INVENTREE_EMAIL_SENDER | email.sender | Name of sender | *Not specified* |
| INVENTREE_EMAIL_SENDER | email.sender | Sending email address | *Not specified* |
| INVENTREE_EMAIL_PREFIX | email.prefix | Prefix for subject text | [InvenTree] |
### Sender Email
The "sender" email address is the address from which InvenTree emails are sent (by default) and must be specified for outgoing emails to function:
!!! info "Fallback"
If `INVENTREE_EMAIL_SENDER` is not provided, the system will fall back to `INVENTREE_EMAIL_USERNAME` (if the username is a valid email address)
## Supported Currencies
The currencies supported by InvenTree must be specified in the [configuration file](#configuration-file).

View File

@@ -35,7 +35,7 @@ sudo apt-get install \
```
!!! warning "Weasyprint"
On some systems, the dependencies for the `weasyprint` package might not be installed. Consider running through the [weasyprint installation steps](https://weasyprint.readthedocs.io/en/stable/install.html) before moving forward.
On some systems, the dependencies for the `weasyprint` package might not be installed. Consider running through the [weasyprint installation steps](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation) before moving forward.
### Create InvenTree User

View File

@@ -86,7 +86,7 @@ pytz==2023.3
# via
# -c requirements.txt
# django
pyyaml==6.0
pyyaml==6.0.1
# via
# -c requirements.txt
# pre-commit

View File

@@ -40,6 +40,7 @@ pillow # Image manipulation
pint==0.21 # Unit conversion # FIXED 2023-05-30 breaks tests https://github.com/matmair/InvenTree/actions/runs/5095665936/jobs/9160852560
python-barcode[images] # Barcode generator
python-dotenv # Environment variable management
pyyaml>=6.0.1 # YAML parsing
qrcode[pil] # QR code generator
rapidfuzz==0.7.6 # Fuzzy string matching
regex # Advanced regular expressions

View File

@@ -239,8 +239,9 @@ pytz==2023.3
# django-dbbackup
# djangorestframework
# icalendar
pyyaml==6.0
pyyaml==6.0.1
# via
# -r requirements.in
# drf-spectacular
# tablib
qrcode[pil]==7.4.2