Compare commits

...

17 Commits

Author SHA1 Message Date
github-actions[bot]
eed6223187 Fix default value for SESSION_COOKIE_SECURE (#8767) (#8769)
- Default value was previously 'True'
- Documentation indicated that it was 'False'
- Value in config_template.yaml was 'False' (but commented out)

(cherry picked from commit d4ee8c53b2)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-26 22:40:26 +11:00
github-actions[bot]
cab7a06146 Zero stock fix (#8766) (#8768)
* Change backend validation

- Allow stock adjustments with zero quantity

* Frontend changes

(cherry picked from commit ae7f4e33d5)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-26 11:26:04 +11:00
Matthias Mair
40245a6c4a [0.17.x] Fix REST registration endpoint (#8738) (#8763)
* Fix REST registration endpoint (#8738)

* Re-add html account base
Fixes #8690

* fix base template

* override dj-rest-auth pattern to fix fixed token model reference

* pin req

* fix urls.py

* move definition out to separate file

* fix possible issues where email is not enabled but UI shows that registration is enabled

* fix import order

* fix token recovery

* make sure registration redirects

* fix name change

* fix import name

* adjust description

* cleanup

* bum api version

* add test for registration

* add test for registration requirements

* fix merge issues

* fix merge from https://github.com/inventree/InvenTree/pull/8724
2024-12-25 11:38:02 +11:00
github-actions[bot]
3cb806d20a Handle error when loading icon pack (#8753) (#8755)
* Handle error when loading icon pack

* Update

(cherry picked from commit 8fcebefa0b)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-24 10:29:24 +11:00
github-actions[bot]
8f1bf95463 Add separate dialog for 'Ship Order' button (#8734) (#8735)
(cherry picked from commit 0bcad6b340)

Co-authored-by: Joe Rogers <1337joe@users.noreply.github.com>
2024-12-22 16:12:57 +11:00
github-actions[bot]
4019dc9c9c Forms fixes (#8722) (#8729)
* Refactor form fields

- Allow error message to be passed through via field definition
- Return error information to onFormError

* Fix debounce issue for text fields

* Fix for useForm hook

* Badge fix

- Fix badge rendering for SalesOrderShipment

* Cleanup unit test

(cherry picked from commit aabcf52cd2)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-20 15:52:06 +11:00
github-actions[bot]
70f17997eb [UI] Link fix (#8726) (#8728)
- Fixes anchor issues in stock tracking table

(cherry picked from commit 68ac4118e9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-20 12:28:49 +11:00
github-actions[bot]
2d773a7b3e Badge fix (#8725) (#8727)
- Fix badge rendering for SalesOrderShipment

(cherry picked from commit 130bc84b44)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-20 12:21:42 +11:00
Oliver
39211ff4b6 Fix MFA auth flow (#8720) (#8724)
* Re-add html account base
Fixes #8690

* fix base template

Co-authored-by: Matthias Mair <code@mjmair.com>
2024-12-20 09:12:14 +11:00
github-actions[bot]
e37ff5c3d5 [UI] Enhanced null checks (#8706) (#8711)
* Extra null check in SettingList.tsx

* Null checks on error responses

(cherry picked from commit 378d69f0b3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-19 01:01:51 +11:00
github-actions[bot]
6bd32c9236 Image upload error (#8700) (#8704)
* Add helper function for displaying API error message

* Provide feedback on image upload

* Update notification

(cherry picked from commit 1eaf3a4594)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-19 00:16:39 +11:00
github-actions[bot]
04aec83e95 Cast barcode scan IDs to list (#8701) (#8702)
- Fixes issues with limitations on old MySQL server

(cherry picked from commit 4569fd273d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-18 17:04:18 +11:00
github-actions[bot]
b57d035f7f Fix for table update (#8698) (#8699)
- Retain data when updating a single record
- Fixes https://github.com/inventree/InvenTree/issues/8693

(cherry picked from commit 1910612725)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-18 15:39:37 +11:00
Oliver
3ac49441ca Update version.py (#8689)
Bump version number to 0.17.1
2024-12-17 22:31:20 +11:00
github-actions[bot]
156c3cc9b2 Bump mkdocs-material from 9.5.48 to 9.5.49 in /docs in the dependencies group across 1 directory (#8675) (#8678)
* Bump mkdocs-material

Bumps the dependencies group with 1 update in the /docs directory: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).

Updates `mkdocs-material` from 9.5.48 to 9.5.49
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.48...9.5.49)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix req

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
(cherry picked from commit 1518475d51)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 09:16:26 +11:00
github-actions[bot]
52a26c9887 Bump the dependencies group with 5 updates (#8673) (#8679)
Bumps the dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.7.1` | `3.8.0` |
| [anchore/sbom-action](https://github.com/anchore/sbom-action) | `0.17.8` | `0.17.9` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `2.0.1` | `2.1.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3.27.6` | `3.27.9` |
| [crowdin/github-action](https://github.com/crowdin/github-action) | `2.4.0` | `2.5.0` |

Updates `docker/setup-buildx-action` from 3.7.1 to 3.8.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](c47758b77c...6524bf65af)

Updates `anchore/sbom-action` from 0.17.8 to 0.17.9
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](55dc4ee224...df80a981bc)

Updates `actions/attest-build-provenance` from 2.0.1 to 2.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](c4fbc64884...7668571508)

Updates `github/codeql-action` from 3.27.6 to 3.27.9
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](aa57810251...df409f7d92)

Updates `crowdin/github-action` from 2.4.0 to 2.5.0
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](a9ffb7d5ac...8dfaf9c206)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: crowdin/github-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 1e4e3e65cc)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 09:16:23 +11:00
Oliver
667e0a1bee Update version number (#8676) 2024-12-17 09:15:43 +11:00
42 changed files with 309 additions and 107 deletions

View File

@@ -127,7 +127,7 @@ jobs:
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # pin@v3.2.0
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # pin@v3.7.1
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # pin@v3.8.0
- name: Set up cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # pin@v3.7.0

View File

@@ -49,7 +49,7 @@ jobs:
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend
uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # pin@v0
uses: anchore/sbom-action@df80a981bc6edbc4e220a492d3cbe9f5547a6e75 # pin@v0
with:
artifact-name: frontend-build.spdx
path: src/frontend
@@ -63,7 +63,7 @@ jobs:
zip -r ../frontend-build.zip * .vite
- name: Attest Build Provenance
id: attest
uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # pin@v1
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # pin@v1
with:
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"

View File

@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
with:
sarif_file: results.sarif

View File

@@ -51,7 +51,7 @@ jobs:
git reset --hard
git reset HEAD~
- name: crowdin action
uses: crowdin/github-action@a9ffb7d5ac46eca1bb1f06656bf888b39462f161 # pin@v2
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # pin@v2
with:
upload_sources: true
upload_translations: false

View File

@@ -319,9 +319,9 @@ mkdocs-macros-plugin==1.3.7 \
--hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \
--hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523
# via -r docs/requirements.in
mkdocs-material==9.5.48 \
--hash=sha256:a582531e8b34f4c7ed38c29d5c44763053832cf2a32f7409567e0c74749a47db \
--hash=sha256:b695c998f4b939ce748adbc0d3bff73fa886a670ece948cf27818fa115dc16f8
mkdocs-material==9.5.49 \
--hash=sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d \
--hash=sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \

View File

@@ -1,13 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 293
INVENTREE_API_VERSION = 294
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v294 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8738
- Extends registration API documentation
v293 - 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658
- Adds new fields to the supplier barcode API endpoints

View File

@@ -0,0 +1,40 @@
"""Overrides for registration view."""
from django.utils.translation import gettext_lazy as _
from allauth.account import app_settings as allauth_account_settings
from dj_rest_auth.app_settings import api_settings
from dj_rest_auth.registration.views import RegisterView
class CustomRegisterView(RegisterView):
"""Registers a new user.
Accepts the following POST parameters: username, email, password1, password2.
"""
# Fixes https://github.com/inventree/InvenTree/issues/8707
# This contains code from dj-rest-auth 7.0 - therefore the version was pinned
def get_response_data(self, user):
"""Override to fix check for auth_model."""
if (
allauth_account_settings.EMAIL_VERIFICATION
== allauth_account_settings.EmailVerificationMethod.MANDATORY
):
return {'detail': _('Verification e-mail sent.')}
if api_settings.USE_JWT:
data = {
'user': user,
'access': self.access_token,
'refresh': self.refresh_token,
}
return api_settings.JWT_SERIALIZER(
data, context=self.get_serializer_context()
).data
elif self.token_model:
# Only change in this block is below
return api_settings.TOKEN_SERIALIZER(
user.api_tokens.last(), context=self.get_serializer_context()
).data
return None

View File

@@ -20,7 +20,9 @@ from allauth_2fa.utils import user_has_valid_totp_device
from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Field, Layout
from dj_rest_auth.registration.serializers import RegisterSerializer
from dj_rest_auth.registration.serializers import (
RegisterSerializer as DjRestRegisterSerializer,
)
from rest_framework import serializers
import InvenTree.helpers_model
@@ -385,16 +387,11 @@ class CustomSocialAccountAdapter(
# override dj-rest-auth
class CustomRegisterSerializer(RegisterSerializer):
"""Override of serializer to use dynamic settings."""
class RegisterSerializer(DjRestRegisterSerializer):
"""Registration requires email, password (twice) and username."""
email = serializers.EmailField()
def __init__(self, instance=None, data=..., **kwargs):
"""Check settings to influence which fields are needed."""
kwargs['email_required'] = get_global_setting('LOGIN_MAIL_REQUIRED')
super().__init__(instance, data, **kwargs)
def save(self, request):
"""Override to check if registration is open."""
if registration_enabled():

View File

@@ -620,12 +620,10 @@ REST_AUTH = {
'TOKEN_MODEL': 'users.models.ApiToken',
'TOKEN_CREATOR': 'users.models.default_create_token',
'USE_JWT': USE_JWT,
'REGISTER_SERIALIZER': 'InvenTree.forms.RegisterSerializer',
}
OLD_PASSWORD_FIELD_ENABLED = True
REST_AUTH_REGISTER_SERIALIZERS = {
'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer'
}
# JWT settings - rest_framework_simplejwt
if USE_JWT:
@@ -1220,7 +1218,9 @@ SESSION_COOKIE_SECURE = (
if DEBUG
else (
SESSION_COOKIE_SAMESITE == 'None'
or get_boolean_setting('INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', True)
or get_boolean_setting(
'INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', False
)
)
)

View File

@@ -18,6 +18,7 @@ from rest_framework.response import Response
import InvenTree.sso
from common.settings import get_global_setting
from InvenTree.forms import registration_enabled
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI
from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer
@@ -204,7 +205,7 @@ class SocialProviderListView(ListAPI):
and get_global_setting('LOGIN_ENFORCE_MFA'),
'mfa_enabled': settings.MFA_ENABLED,
'providers': provider_list,
'registration_enabled': get_global_setting('LOGIN_ENABLE_REG'),
'registration_enabled': registration_enabled(),
'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'),
}
return Response(data)

View File

@@ -1,6 +1,8 @@
"""Test the sso module functionality."""
"""Test the sso and auth module functionality."""
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.core.exceptions import ValidationError
from django.test import override_settings
from django.test.testcases import TransactionTestCase
@@ -9,6 +11,7 @@ from allauth.socialaccount.models import SocialAccount, SocialLogin
from common.models import InvenTreeSetting
from InvenTree import sso
from InvenTree.forms import RegistratonMixin
from InvenTree.unit_test import InvenTreeAPITestCase
class Dummy:
@@ -119,3 +122,90 @@ class TestSsoGroupSync(TransactionTestCase):
self.assertEqual(Group.objects.filter(name='inventree_group').count(), 0)
sso.ensure_sso_groups(None, self.sociallogin)
self.assertEqual(Group.objects.filter(name='inventree_group').count(), 1)
class EmailSettingsContext:
"""Context manager to enable email settings for tests."""
def __enter__(self):
"""Enable stuff."""
InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', True)
settings.EMAIL_HOST = 'localhost'
def __exit__(self, type, value, traceback):
"""Exit stuff."""
InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', False)
settings.EMAIL_HOST = ''
class TestAuth(InvenTreeAPITestCase):
"""Test authentication functionality."""
def email_args(self, user=None, email=None):
"""Generate registration arguments."""
return {
'username': user or 'user1',
'email': email or 'test@example.com',
'password1': '#asdf1234',
'password2': '#asdf1234',
}
def test_registration(self):
"""Test the registration process."""
self.logout()
# Duplicate username
resp = self.post(
'/api/auth/registration/',
self.email_args(user='testuser'),
expected_code=400,
)
self.assertIn(
'A user with that username already exists.', resp.data['username']
)
# Registration is disabled
resp = self.post(
'/api/auth/registration/', self.email_args(), expected_code=400
)
self.assertIn('Registration is disabled.', resp.data['non_field_errors'])
# Enable registration - now it should work
with EmailSettingsContext():
resp = self.post(
'/api/auth/registration/', self.email_args(), expected_code=201
)
self.assertIn('key', resp.data)
def test_registration_email(self):
"""Test that LOGIN_SIGNUP_MAIL_RESTRICTION works."""
self.logout()
# Check the setting validation is working
with self.assertRaises(ValidationError):
InvenTreeSetting.set_setting(
'LOGIN_SIGNUP_MAIL_RESTRICTION', 'example.com,inventree.org'
)
# Setting setting correctly
correct_setting = '@example.com,@inventree.org'
InvenTreeSetting.set_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', correct_setting)
self.assertEqual(
InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_RESTRICTION'),
correct_setting,
)
# Wrong email format
resp = self.post(
'/api/auth/registration/',
self.email_args(email='admin@invenhost.com'),
expected_code=400,
)
self.assertIn('The provided email domain is not approved.', resp.data['email'])
# Right format should work
with EmailSettingsContext():
resp = self.post(
'/api/auth/registration/', self.email_args(), expected_code=201
)
self.assertIn('key', resp.data)

View File

@@ -32,6 +32,7 @@ import users.api
from build.urls import build_urls
from common.urls import common_urls
from company.urls import company_urls, manufacturer_part_urls, supplier_part_urls
from InvenTree.auth_override_views import CustomRegisterView
from order.urls import order_urls
from part.urls import part_urls
from plugin.urls import get_plugin_urls
@@ -202,6 +203,7 @@ apipatterns = [
ConfirmEmailView.as_view(),
name='account_confirm_email',
),
path('registration/', CustomRegisterView.as_view(), name='rest_register'),
path('registration/', include('dj_rest_auth.registration.urls')),
path(
'providers/', SocialProviderListView.as_view(), name='social_providers'

View File

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

View File

@@ -124,9 +124,9 @@ use_x_forwarded_host: false
use_x_forwarded_port: false
# Cookie settings (nominally the default settings should be fine)
#cookie:
# secure: false
# samesite: false
cookie:
secure: false
samesite: false
# Cross Origin Resource Sharing (CORS) settings (see https://github.com/adamchainz/django-cors-headers)
cors:

View File

@@ -92,7 +92,7 @@ class BarcodeView(CreateAPIView):
if num_scans > max_scans:
n = num_scans - max_scans
old_scan_ids = (
old_scan_ids = list(
BarcodeScanResult.objects.all()
.order_by('timestamp')
.values_list('pk', flat=True)[:n]

View File

@@ -1508,17 +1508,22 @@ class StockItem(
"""
return self.children.count()
def is_in_stock(self, check_status: bool = True):
def is_in_stock(
self, check_status: bool = True, check_quantity: bool = True
) -> bool:
"""Return True if this StockItem is "in stock".
Args:
check_status: If True, check the status of the StockItem. Defaults to True.
check_quantity: If True, check the quantity of the StockItem. Defaults to True.
"""
if check_status and self.status not in StockStatusGroups.AVAILABLE_CODES:
return False
if check_quantity and self.quantity <= 0:
return False
return all([
self.quantity > 0, # Quantity must be greater than zero
self.sales_order is None, # Not assigned to a SalesOrder
self.belongs_to is None, # Not installed inside another StockItem
self.customer is None, # Not assigned to a customer

View File

@@ -1571,7 +1571,9 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False
)
if not allow_out_of_stock_transfer and not pk.is_in_stock(check_status=False):
if not allow_out_of_stock_transfer and not pk.is_in_stock(
check_status=False, check_quantity=False
):
raise ValidationError(_('Stock item is not in stock'))
return pk

View File

@@ -1,4 +1,4 @@
{% extends "account/base.html" %}
{% extends "skeleton.html" %}
{% load i18n %}
{% load inventree_extras %}

View File

@@ -5,11 +5,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="57x57" href="{% static 'img/favicon/apple-icon-57x57.png' %}">
<link rel="apple-touch-icon" sizes="60x60" href="{% static 'img/favicon/apple-icon-60x60.png' %}">
@@ -28,8 +26,6 @@
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="{% static 'img/favicon/ms-icon-144x144.png' %}">
<meta name="theme-color" content="#ffffff">
<!-- CSS -->
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
@@ -37,13 +33,10 @@
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2-bootstrap-5-theme.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
<link rel="stylesheet" href="{% get_color_theme_css request.user %}">
<title>
{% inventree_title %} | {% block head_title %}{% endblock head_title %}
</title>
{% block extra_head %}
{% endblock extra_head %}
</head>
@@ -85,37 +78,26 @@
<!-- general JS -->
{% include "third_party_js.html" %}
<script type='text/javascript' src='{% static "script/inventree/inventree.js" %}'></script>
<script type='text/javascript' src='{% static "script/inventree/message.js" %}'></script>
<script type='text/javascript'>
$(document).ready(function () {
{% if messages %}
{% for message in messages %}
showMessage("{{ message }}");
{% endfor %}
{% endif %}
showCachedAlerts();
// Add brand icons for SSO providers, if available
$('.socialaccount_provider').each(function(i, obj) {
var el = $(this);
var tag = el.attr('brand_name');
var icon = window.FontAwesome.icon({prefix: 'fab', iconName: tag});
if (icon) {
el.prepend(`<span class='fab fa-${tag}'></span>&nbsp;`);
}
});
});
</script>
</body>
</html>

View File

@@ -34,7 +34,7 @@ django-weasyprint # django weasyprint integration
djangorestframework<3.15 # DRF framework # FIXED 2024-06-26 see https://github.com/inventree/InvenTree/pull/7521
djangorestframework-simplejwt[crypto] # JWT authentication
django-xforwardedfor-middleware # IP forwarding metadata
dj-rest-auth # Authentication API endpoints
dj-rest-auth==7.0.0 # Authentication API endpoints # FIXED 2024-12-22 due to https://github.com/inventree/InvenTree/issues/8707
dulwich # pure Python git integration
drf-spectacular # DRF API documentation
feedparser # RSS newsfeed parser

View File

@@ -19,10 +19,12 @@ import { useHover } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useMemo, useState } from 'react';
import { showNotification } from '@mantine/notifications';
import { api } from '../../App';
import type { UserRoles } from '../../enums/Roles';
import { cancelEvent } from '../../functions/events';
import { InvenTreeIcon } from '../../functions/icons';
import { showApiErrorMessage } from '../../functions/notifications';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { useGlobalSettingsState } from '../../states/SettingsState';
import { useUserState } from '../../states/UserState';
@@ -159,12 +161,24 @@ function UploadModal({
const formData = new FormData();
formData.append('image', file, file.name);
const response = await api.patch(apiPath, formData);
if (response.data.image.includes(file.name)) {
setImage(response.data.image);
modals.closeAll();
}
api
.patch(apiPath, formData)
.then((response) => {
setImage(response.data.image);
modals.closeAll();
showNotification({
title: t`Image uploaded`,
message: t`Image has been uploaded successfully`,
color: 'green'
});
})
.catch((error) => {
showApiErrorMessage({
error: error,
title: t`Upload Error`,
field: 'image'
});
});
};
const { colorScheme } = useMantineColorScheme();

View File

@@ -94,7 +94,7 @@ export interface ApiFormProps {
postFormContent?: JSX.Element;
successMessage?: string;
onFormSuccess?: (data: any) => void;
onFormError?: () => void;
onFormError?: (response: any) => void;
processFormData?: (data: any) => any;
table?: TableState;
modelType?: ModelType;
@@ -482,7 +482,7 @@ export function ApiForm({
default:
// Unexpected state on form success
invalidResponse(response.status);
props.onFormError?.();
props.onFormError?.(response);
break;
}
@@ -534,26 +534,30 @@ export function ApiForm({
processErrors(error.response.data);
setNonFieldErrors(_nonFieldErrors);
props.onFormError?.(error);
break;
default:
// Unexpected state on form error
invalidResponse(error.response.status);
props.onFormError?.();
props.onFormError?.(error);
break;
}
} else {
showTimeoutNotification();
props.onFormError?.();
props.onFormError?.(error);
}
return error;
});
};
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(() => {
props.onFormError?.();
}, [props.onFormError]);
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(
(error: any) => {
props.onFormError?.(error);
},
[props.onFormError]
);
if (optionsLoading || initialDataQuery.isFetching) {
return (

View File

@@ -192,7 +192,7 @@ export function RegistrationForm() {
headers: { Authorization: '' }
})
.then((ret) => {
if (ret?.status === 204) {
if (ret?.status === 204 || ret?.status === 201) {
setIsRegistering(false);
showLoginNotification({
title: t`Registration successful`,
@@ -202,7 +202,7 @@ export function RegistrationForm() {
}
})
.catch((err) => {
if (err.response.status === 400) {
if (err.response?.status === 400) {
setIsRegistering(false);
for (const [key, value] of Object.entries(err.response.data)) {
registrationForm.setFieldError(key, value as string);

View File

@@ -46,6 +46,7 @@ export type ApiFormFieldChoice = {
* @param required : Whether the field is required
* @param hidden : Whether the field is hidden
* @param disabled : Whether the field is disabled
* @param error : Optional error message to display
* @param exclude : Whether to exclude the field from the submitted data
* @param placeholder : The placeholder text to display
* @param description : The description to display for the field
@@ -88,6 +89,7 @@ export type ApiFormFieldType = {
child?: ApiFormFieldType;
children?: { [key: string]: ApiFormFieldType };
required?: boolean;
error?: string;
choices?: ApiFormFieldChoice[];
hidden?: boolean;
disabled?: boolean;
@@ -256,7 +258,7 @@ export function ApiFormField({
aria-label={`boolean-field-${fieldName}`}
radius='lg'
size='sm'
error={error?.message}
error={definition.error ?? error?.message}
onChange={(event) => onChange(event.currentTarget.checked)}
/>
);
@@ -277,7 +279,7 @@ export function ApiFormField({
id={fieldId}
aria-label={`number-field-${field.name}`}
value={numericalValue}
error={error?.message}
error={definition.error ?? error?.message}
decimalScale={definition.field_type == 'integer' ? 0 : 10}
onChange={(value: number | string | null) => onChange(value)}
step={1}
@@ -299,7 +301,7 @@ export function ApiFormField({
ref={field.ref}
radius='sm'
value={value}
error={error?.message}
error={definition.error ?? error?.message}
onChange={(payload: File | null) => onChange(payload)}
/>
);
@@ -343,6 +345,7 @@ export function ApiFormField({
booleanValue,
control,
controller,
definition,
field,
fieldId,
fieldName,

View File

@@ -63,7 +63,7 @@ export function ChoiceField({
<Select
id={fieldId}
aria-label={`choice-field-${field.name}`}
error={error?.message}
error={definition.error ?? error?.message}
radius='sm'
{...field}
onChange={onChange}

View File

@@ -61,7 +61,7 @@ export default function DateField({
radius='sm'
ref={field.ref}
type={undefined}
error={error?.message}
error={definition.error ?? error?.message}
value={dateValue ?? null}
clearable={!definition.required}
onChange={onChange}

View File

@@ -50,7 +50,7 @@ export default function IconField({
label={definition.label}
description={definition.description}
required={definition.required}
error={error?.message}
error={definition.error ?? error?.message}
ref={field.ref}
component='button'
type='button'

View File

@@ -284,7 +284,7 @@ export function RelatedModelField({
return (
<Input.Wrapper
{...fieldDefinition}
error={error?.message}
error={definition.error ?? error?.message}
styles={{ description: { paddingBottom: '5px' } }}
>
<Select

View File

@@ -258,7 +258,7 @@ export function TableFieldExtraRow({
fieldName={fieldName ?? 'field'}
fieldDefinition={field}
defaultValue={defaultValue}
error={error}
error={fieldDefinition.error ?? error}
/>
</Group>
</Table.Td>

View File

@@ -56,7 +56,7 @@ export default function TextField({
aria-label={`text-field-${field.name}`}
type={definition.field_type}
value={rawText || ''}
error={error?.message}
error={definition.error ?? error?.message}
radius='sm'
onChange={(event) => onTextChange(event.currentTarget.value)}
onBlur={(event) => {
@@ -64,7 +64,13 @@ export default function TextField({
onChange(event.currentTarget.value);
}
}}
onKeyDown={(event) => onKeyDown(event.code)}
onKeyDown={(event) => {
if (event.code === 'Enter') {
// Bypass debounce on enter key
onChange(event.currentTarget.value);
}
onKeyDown(event.code);
}}
rightSection={
value && !definition.required ? (
<IconX size='1rem' color='red' onClick={() => onTextChange('')} />

View File

@@ -199,7 +199,7 @@ export function RenderInlineModel({
{prefix}
{image && <Thumbnail src={image} size={18} />}
{url ? (
<Anchor href={url} onClick={(event: any) => onClick(event)}>
<Anchor href='' onClick={(event: any) => onClick(event)}>
<Text size='sm'>{primary}</Text>
</Anchor>
) : (

View File

@@ -158,7 +158,7 @@ export function SettingList({
</React.Fragment>
);
})}
{(keys || allKeys).length === 0 && (
{(keys || allKeys)?.length === 0 && (
<Text style={{ fontStyle: 'italic' }}>
<Trans>No settings specified</Trans>
</Text>

View File

@@ -74,3 +74,31 @@ export function showLoginNotification({
autoClose: 2500
});
}
export function showApiErrorMessage({
error,
title,
message,
field
}: {
error: any;
title: string;
message?: string;
field?: string;
}) {
// Extract error description from response
const error_data: any = error.response?.data ?? {};
let error_msg: any =
message ?? error_data[field ?? 'error'] ?? error_data['non_field_errors'];
if (!error_msg) {
error_msg = t`An error occurred`;
}
notifications.show({
title: title,
message: error_msg,
color: 'red'
});
}

View File

@@ -48,9 +48,8 @@ export function useApiFormModal(props: ApiFormModalProps) {
modalClose.current();
props.onFormSuccess?.(data);
},
onFormError: () => {
modalClose.current();
props.onFormError?.();
onFormError: (error: any) => {
props.onFormError?.(error);
}
}),
[props]

View File

@@ -167,7 +167,10 @@ export function useTable(tableName: string): TableState {
const index = _records.findIndex((r) => r.pk === record.pk);
if (index >= 0) {
_records[index] = record;
_records[index] = {
..._records[index],
...record
};
} else {
_records.push(record);
}

View File

@@ -77,8 +77,8 @@ export default function Set_Password() {
})
.catch((err) => {
if (
err.response.status === 400 &&
err.response.data?.token == 'Invalid value'
err.response?.status === 400 &&
err.response?.data?.token == 'Invalid value'
) {
invalidToken();
} else {

View File

@@ -9,6 +9,7 @@ import { ActionButton } from '../../../../components/buttons/ActionButton';
import { FactCollection } from '../../../../components/settings/FactCollection';
import { GlobalSettingList } from '../../../../components/settings/SettingList';
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
import { showApiErrorMessage } from '../../../../functions/notifications';
import { useTable } from '../../../../hooks/UseTable';
import { apiUrl } from '../../../../states/ApiState';
import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
@@ -46,10 +47,9 @@ export function CurrencyTable({
});
})
.catch((error) => {
showNotification({
title: t`Exchange rate update error`,
message: error,
color: 'red'
showApiErrorMessage({
error: error,
title: t`Exchange rate update error`
});
});
}, []);

View File

@@ -399,6 +399,17 @@ export default function SalesOrderDetail() {
successMessage: t`Order placed on hold`
});
const shipOrder = useCreateApiFormModal({
url: apiUrl(ApiEndpoints.sales_order_complete, order.pk),
title: t`Ship Sales Order`,
onFormSuccess: refreshInstance,
preFormWarning: t`Ship this order?`,
successMessage: t`Order shipped`,
fields: {
accept_incomplete: {}
}
});
const completeOrder = useCreateApiFormModal({
url: apiUrl(ApiEndpoints.sales_order_complete, order.pk),
title: t`Complete Sales Order`,
@@ -444,7 +455,7 @@ export default function SalesOrderDetail() {
icon='deliver'
hidden={!canShip}
color='blue'
onClick={completeOrder.open}
onClick={shipOrder.open}
/>,
<PrimaryActionButton
title={t`Complete Order`}
@@ -510,6 +521,7 @@ export default function SalesOrderDetail() {
{issueOrder.modal}
{cancelOrder.modal}
{holdOrder.modal}
{shipOrder.modal}
{completeOrder.modal}
{editSalesOrder.modal}
{duplicateSalesOrder.modal}

View File

@@ -295,7 +295,7 @@ export default function SalesOrderShipmentDetail() {
visible={!!shipment.delivery_date}
/>
];
}, [shipment, shipmentQuery]);
}, [isPending, shipment.deliveryDate, shipmentQuery.isFetching]);
const shipmentActions = useMemo(() => {
const canEdit: boolean = user.hasChangePermission(

View File

@@ -661,7 +661,6 @@ export default function StockDetail() {
const stockActions = useMemo(() => {
const inStock =
user.hasChangeRole(UserRoles.stock) &&
stockitem.quantity > 0 &&
!stockitem.sales_order &&
!stockitem.belongs_to &&
!stockitem.customer &&
@@ -717,7 +716,7 @@ export default function StockDetail() {
{
name: t`Remove`,
tooltip: t`Remove Stock`,
hidden: serialized || !inStock,
hidden: serialized || !inStock || stockitem.quantity <= 0,
icon: <InvenTreeIcon icon='remove' iconProps={{ color: 'red' }} />,
onClick: () => {
stockitem.pk && removeStockItem.open();

View File

@@ -1,5 +1,7 @@
import { create } from 'zustand';
import { t } from '@lingui/macro';
import { showNotification } from '@mantine/notifications';
import { api } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { generateUrl } from '../functions/urls';
@@ -38,17 +40,29 @@ export const useIconState = create<IconState>()((set, get) => ({
await Promise.all(
packs.data.map(async (pack: any) => {
const fontName = `inventree-icon-font-${pack.prefix}`;
const src = Object.entries(pack.fonts as Record<string, string>)
.map(
([format, url]) => `url(${generateUrl(url)}) format("${format}")`
)
.join(',\n');
const font = new FontFace(fontName, `${src};`);
await font.load();
document.fonts.add(font);
if (pack.prefix && pack.fonts) {
const fontName = `inventree-icon-font-${pack.prefix}`;
const src = Object.entries(pack.fonts as Record<string, string>)
.map(
([format, url]) => `url(${generateUrl(url)}) format("${format}")`
)
.join(',\n');
const font = new FontFace(fontName, `${src};`);
await font.load();
document.fonts.add(font);
return font;
} else {
console.error(
"ERR: Icon package is missing 'prefix' or 'fonts' field"
);
showNotification({
title: t`Error`,
message: t`Error loading icon package from server`,
color: 'red'
});
return font;
return null;
}
})
);
@@ -56,7 +70,7 @@ export const useIconState = create<IconState>()((set, get) => ({
hasLoaded: true,
packages: packs.data,
packagesMap: Object.fromEntries(
packs.data.map((pack: any) => [pack.prefix, pack])
packs.data?.map((pack: any) => [pack.prefix, pack])
)
});
}

View File

@@ -90,10 +90,8 @@ test('Build Order - Basic Tests', async ({ page }) => {
test('Build Order - Build Outputs', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/`);
// Navigate to the correct build order
await page.getByRole('tab', { name: 'Manufacturing', exact: true }).click();
await page.goto(`${baseUrl}/manufacturing/index/`);
await page.getByRole('tab', { name: 'Build Orders', exact: true }).click();
// We have now loaded the "Build Order" table. Check for some expected texts
await page.getByText('On Hold').first().waitFor();