mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-18 12:56:31 -06:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eed6223187 | ||
|
|
cab7a06146 | ||
|
|
40245a6c4a | ||
|
|
3cb806d20a | ||
|
|
8f1bf95463 | ||
|
|
4019dc9c9c | ||
|
|
70f17997eb | ||
|
|
2d773a7b3e | ||
|
|
39211ff4b6 | ||
|
|
e37ff5c3d5 | ||
|
|
6bd32c9236 | ||
|
|
04aec83e95 | ||
|
|
b57d035f7f | ||
|
|
3ac49441ca | ||
|
|
156c3cc9b2 | ||
|
|
52a26c9887 | ||
|
|
667e0a1bee |
2
.github/workflows/docker.yaml
vendored
2
.github/workflows/docker.yaml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -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"
|
||||
|
||||
|
||||
2
.github/workflows/scorecard.yaml
vendored
2
.github/workflows/scorecard.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
40
src/backend/InvenTree/InvenTree/auth_override_views.py
Normal file
40
src/backend/InvenTree/InvenTree/auth_override_views.py
Normal 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
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "account/base.html" %}
|
||||
{% extends "skeleton.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
@@ -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> `);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -284,7 +284,7 @@ export function RelatedModelField({
|
||||
return (
|
||||
<Input.Wrapper
|
||||
{...fieldDefinition}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
styles={{ description: { paddingBottom: '5px' } }}
|
||||
>
|
||||
<Select
|
||||
|
||||
@@ -258,7 +258,7 @@ export function TableFieldExtraRow({
|
||||
fieldName={fieldName ?? 'field'}
|
||||
fieldDefinition={field}
|
||||
defaultValue={defaultValue}
|
||||
error={error}
|
||||
error={fieldDefinition.error ?? error}
|
||||
/>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
@@ -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('')} />
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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])
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user