Merge branch 'inventree:master' into pr/ChristianSchindler/6305

This commit is contained in:
Matthias Mair 2024-04-02 19:25:36 +01:00 committed by GitHub
commit 839d9f9075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
113 changed files with 54301 additions and 52524 deletions

View File

@ -1,33 +0,0 @@
version = 1
exclude_patterns = [
"docs/docs/javascripts/**", # Docs: Helpers
"docs/ci/**", # Docs: CI
"InvenTree/InvenTree/static/**", # Backend: CUI static files
"ci/**", # Backend: CI
"InvenTree/**/migrations/*.py", # Backend: Migration files
"src/frontend/src/locales/**", # Frontend: Translations
]
test_patterns = ["**/test_*.py", "**/test.py", "**/tests.py"]
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"
[[analyzers]]
name = "docker"
[[analyzers]]
name = "test-coverage"
enabled = false

View File

@ -43,7 +43,9 @@ body:
label: "Deployment Method" label: "Deployment Method"
options: options:
- label: "Docker" - label: "Docker"
- label: "Package"
- label: "Bare metal" - label: "Bare metal"
- label: "Other - added info in Steps to Reproduce"
- type: textarea - type: textarea
id: version-info id: version-info
validations: validations:

View File

@ -16,6 +16,8 @@ jobs:
backport: backport:
name: Backport PR name: Backport PR
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
if: | if: |
github.event.pull_request.merged == true github.event.pull_request.merged == true
&& contains(github.event.pull_request.labels.*.name, 'backport') && contains(github.event.pull_request.labels.*.name, 'backport')

View File

@ -27,6 +27,7 @@ jobs:
INVENTREE_MEDIA_ROOT: ./media INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static INVENTREE_STATIC_ROOT: ./static
INVENTREE_BACKUP_DIR: ./backup INVENTREE_BACKUP_DIR: ./backup
INVENTREE_SITE_URL: http://localhost:8000
steps: steps:
- name: Checkout Code - name: Checkout Code

View File

@ -76,8 +76,8 @@ jobs:
python-version: ${{ env.python_version }} python-version: ${{ env.python_version }}
- name: Version Check - name: Version Check
run: | run: |
pip install requests pip install requests==2.31.0
pip install pyyaml pip install pyyaml==6.0.1
python3 ci/version_check.py python3 ci/version_check.py
echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV

View File

@ -92,7 +92,7 @@ jobs:
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # pin@v3.0.1 uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # pin@v3.0.1
- name: Check Version - name: Check Version
run: | run: |
pip install requests pip install requests==2.31.0
python3 ci/version_check.py python3 ci/version_check.py
mkdocs: mkdocs:
@ -110,7 +110,7 @@ jobs:
python-version: ${{ env.python_version }} python-version: ${{ env.python_version }}
- name: Check Config - name: Check Config
run: | run: |
pip install pyyaml pip install pyyaml==6.0.1
pip install -r docs/requirements.txt pip install -r docs/requirements.txt
python docs/ci/check_mkdocs_config.py python docs/ci/check_mkdocs_config.py
- name: Check Links - name: Check Links
@ -156,7 +156,7 @@ jobs:
- name: Download public schema - name: Download public schema
if: needs.paths-filter.outputs.api == 'false' if: needs.paths-filter.outputs.api == 'false'
run: | run: |
pip install requests >/dev/null 2>&1 pip install requests==2.31.0 >/dev/null 2>&1
version="$(python3 ci/version_check.py only_version 2>&1)" version="$(python3 ci/version_check.py only_version 2>&1)"
echo "Version: $version" echo "Version: $version"
url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml" url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml"
@ -175,7 +175,7 @@ jobs:
id: version id: version
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true' if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
run: | run: |
pip install requests >/dev/null 2>&1 pip install requests==2.31.0 >/dev/null 2>&1
version="$(python3 ci/version_check.py only_version 2>&1)" version="$(python3 ci/version_check.py only_version 2>&1)"
echo "Version: $version" echo "Version: $version"
echo "version=$version" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT"

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
- name: Version Check - name: Version Check
run: | run: |
pip install requests pip install requests==2.31.0
python3 ci/version_check.py python3 ci/version_check.py
- name: Push to Stable Branch - name: Push to Stable Branch
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0 uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0

View File

@ -54,7 +54,7 @@ jobs:
# For private repositories: # For private repositories:
# - `publish_results` will always be set to `false`, regardless # - `publish_results` will always be set to `false`, regardless
# of the value entered here. # of the value entered here.
publish_results: false publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.

View File

@ -10,12 +10,14 @@ env:
node_version: 18 node_version: 18
permissions: permissions:
contents: write contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -10,7 +10,7 @@
# - Monitors source files for any changes, and live-reloads server # - Monitors source files for any changes, and live-reloads server
ARG base_image=python:3.11-alpine3.18 ARG base_image=python:3.11-alpine3.18
FROM ${base_image} as inventree_base FROM ${base_image} AS inventree_base
# Build arguments for this image # Build arguments for this image
ARG commit_tag="" ARG commit_tag=""
@ -92,7 +92,7 @@ RUN chmod +x init.sh
ENTRYPOINT ["/bin/ash", "./init.sh"] ENTRYPOINT ["/bin/ash", "./init.sh"]
FROM inventree_base as prebuild FROM inventree_base AS prebuild
ENV PATH=/root/.local/bin:$PATH ENV PATH=/root/.local/bin:$PATH
RUN ./install_build_packages.sh --no-cache --virtual .build-deps && \ RUN ./install_build_packages.sh --no-cache --virtual .build-deps && \
@ -100,9 +100,9 @@ RUN ./install_build_packages.sh --no-cache --virtual .build-deps && \
apk --purge del .build-deps apk --purge del .build-deps
# Frontend builder image: # Frontend builder image:
FROM prebuild as frontend FROM prebuild AS frontend
RUN apk add --no-cache --update nodejs npm && npm install -g yarn RUN apk add --no-cache --update nodejs npm && npm install -g yarn@v1.22.22
RUN yarn config set network-timeout 600000 -g RUN yarn config set network-timeout 600000 -g
COPY InvenTree ${INVENTREE_HOME}/InvenTree COPY InvenTree ${INVENTREE_HOME}/InvenTree
COPY src ${INVENTREE_HOME}/src COPY src ${INVENTREE_HOME}/src
@ -117,7 +117,7 @@ COPY db_version ${INVENTREE_HOME}/db_version
# InvenTree production image: # InvenTree production image:
# - Copies required files from local directory # - Copies required files from local directory
# - Starts a gunicorn webserver # - Starts a gunicorn webserver
FROM inventree_base as production FROM inventree_base AS production
ENV INVENTREE_DEBUG=False ENV INVENTREE_DEBUG=False
@ -134,11 +134,9 @@ COPY InvenTree ./InvenTree
COPY --from=frontend ${INVENTREE_HOME}/InvenTree/web/static/web ./InvenTree/web/static/web COPY --from=frontend ${INVENTREE_HOME}/InvenTree/web/static/web ./InvenTree/web/static/web
# Launch the production server # Launch the production server
# TODO: Work out why environment variables cannot be interpolated in this command
# TODO: e.g. -b ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT} fails here
CMD gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 --chdir ./InvenTree CMD gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 --chdir ./InvenTree
FROM inventree_base as dev FROM inventree_base AS dev
# Vite server (for local frontend development) # Vite server (for local frontend development)
EXPOSE 5173 EXPOSE 5173
@ -146,11 +144,11 @@ EXPOSE 5173
# Install packages required for building python packages # Install packages required for building python packages
RUN ./install_build_packages.sh RUN ./install_build_packages.sh
RUN pip install uv --no-cache-dir && pip install -r base_requirements.txt --no-cache RUN pip install uv==0.1.26 --no-cache-dir && pip install -r base_requirements.txt --no-cache
# Install nodejs / npm / yarn # Install nodejs / npm / yarn
RUN apk add --no-cache --update nodejs npm && npm install -g yarn RUN apk add --no-cache --update nodejs npm && npm install -g yarn@v1.22.22
RUN yarn config set network-timeout 600000 -g RUN yarn config set network-timeout 600000 -g
# The development image requires the source code to be mounted to /home/inventree/ # The development image requires the source code to be mounted to /home/inventree/

View File

@ -347,7 +347,7 @@ def get_secret_key():
# Create a random key file # Create a random key file
options = string.digits + string.ascii_letters + string.punctuation options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(100)]) key = ''.join([random.choice(options) for _idx in range(100)])
secret_key_file.write_text(key) secret_key_file.write_text(key)
logger.debug("Loading SECRET_KEY from '%s'", secret_key_file) logger.debug("Loading SECRET_KEY from '%s'", secret_key_file)

View File

@ -95,7 +95,7 @@ def from_engineering_notation(value):
""" """
value = str(value).strip() value = str(value).strip()
pattern = f'(\d+)([a-zA-Z]+)(\d+)(.*)' pattern = '(\d+)([a-zA-Z]+)(\d+)(.*)'
if match := re.match(pattern, value): if match := re.match(pattern, value):
left, prefix, right, suffix = match.groups() left, prefix, right, suffix = match.groups()
@ -198,7 +198,6 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
break break
except Exception as exc: except Exception as exc:
value = None value = None
pass
if value is None: if value is None:
if unit: if unit:

View File

@ -20,7 +20,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
name = 'InvenTreeExchange' name = 'InvenTreeExchange'
def get_rates(self, **kwargs) -> None: def get_rates(self, **kwargs) -> dict:
"""Set the requested currency codes and get rates.""" """Set the requested currency codes and get rates."""
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from plugin import registry from plugin import registry

View File

@ -17,11 +17,10 @@ class InvenTreeDateFilter(rest_filters.DateFilter):
def filter(self, qs, value): def filter(self, qs, value):
"""Override the filter method to handle timezones correctly.""" """Override the filter method to handle timezones correctly."""
if settings.USE_TZ: if settings.USE_TZ and value is not None:
if value is not None: tz = timezone.get_current_timezone()
tz = timezone.get_current_timezone() value = datetime(value.year, value.month, value.day)
value = datetime(value.year, value.month, value.day) value = make_aware(value, tz, True)
value = make_aware(value, tz, True)
return super().filter(qs, value) return super().filter(qs, value)

View File

@ -69,7 +69,7 @@ def construct_format_regex(fmt_string: str) -> str:
for group in string.Formatter().parse(fmt_string): for group in string.Formatter().parse(fmt_string):
prefix = group[0] # Prefix (literal text appearing before this group) prefix = group[0] # Prefix (literal text appearing before this group)
name = group[1] # Name of this format variable name = group[1] # Name of this format variable
format = group[2] # Format specifier e.g :04d _fmt = group[2] # Format specifier e.g :04d
rep = [ rep = [
'+', '+',
@ -106,16 +106,16 @@ def construct_format_regex(fmt_string: str) -> str:
# Add a named capture group for the format entry # Add a named capture group for the format entry
if name: if name:
# Check if integer values are required # Check if integer values are required
if format.endswith('d'): if _fmt.endswith('d'):
chr = '\d' c = '\d'
else: else:
chr = '.' c = '.'
# Specify width # Specify width
# TODO: Introspect required width # TODO: Introspect required width
w = '+' w = '+'
pattern += f'(?P<{name}>{chr}{w})' pattern += f'(?P<{name}>{c}{w})'
pattern += '$' pattern += '$'

View File

@ -248,11 +248,7 @@ def str2int(text, default=None):
def is_bool(text): def is_bool(text):
"""Determine if a string value 'looks' like a boolean.""" """Determine if a string value 'looks' like a boolean."""
if str2bool(text, True): return str2bool(text, True) or str2bool(text, False)
return True
elif str2bool(text, False):
return True
return False
def isNull(text): def isNull(text):
@ -473,7 +469,7 @@ def DownloadFile(
return response return response
def increment_serial_number(serial: str): def increment_serial_number(serial):
"""Given a serial number, (attempt to) generate the *next* serial number. """Given a serial number, (attempt to) generate the *next* serial number.
Note: This method is exposed to custom plugins. Note: This method is exposed to custom plugins.
@ -857,9 +853,9 @@ def hash_barcode(barcode_data):
barcode_data = str(barcode_data).strip() barcode_data = str(barcode_data).strip()
barcode_data = remove_non_printable_characters(barcode_data) barcode_data = remove_non_printable_characters(barcode_data)
hash = hashlib.md5(str(barcode_data).encode()) barcode_hash = hashlib.md5(str(barcode_data).encode())
return str(hash.hexdigest()) return str(barcode_hash.hexdigest())
def hash_file(filename: Union[str, Path], storage: Union[Storage, None] = None): def hash_file(filename: Union[str, Path], storage: Union[Storage, None] = None):

View File

@ -7,7 +7,7 @@ from rest_framework import permissions
import users.models import users.models
def get_model_for_view(view, raise_error=True): def get_model_for_view(view):
"""Attempt to introspect the 'model' type for an API view.""" """Attempt to introspect the 'model' type for an API view."""
if hasattr(view, 'get_permission_model'): if hasattr(view, 'get_permission_model'):
return view.get_permission_model() return view.get_permission_model()

View File

@ -426,17 +426,17 @@ TEMPLATES = [
REST_FRAMEWORK = { REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler', 'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
'DATETIME_FORMAT': '%Y-%m-%d %H:%M', 'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': [
'users.authentication.ApiTokenAuthentication', 'users.authentication.ApiTokenAuthentication',
'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
), ],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions', 'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.RolePermission', 'InvenTree.permissions.RolePermission',
), ],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata', 'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer'], 'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer'],
@ -462,8 +462,8 @@ REST_AUTH_REGISTER_SERIALIZERS = {
if USE_JWT: if USE_JWT:
JWT_AUTH_COOKIE = 'inventree-auth' JWT_AUTH_COOKIE = 'inventree-auth'
JWT_AUTH_REFRESH_COOKIE = 'inventree-token' JWT_AUTH_REFRESH_COOKIE = 'inventree-token'
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] + ( REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'].append(
'dj_rest_auth.jwt_auth.JWTCookieAuthentication', 'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
) )
INSTALLED_APPS.append('rest_framework_simplejwt') INSTALLED_APPS.append('rest_framework_simplejwt')
@ -1017,7 +1017,10 @@ if not ALLOWED_HOSTS:
# Ensure that the ALLOWED_HOSTS do not contain any scheme info # Ensure that the ALLOWED_HOSTS do not contain any scheme info
for i, host in enumerate(ALLOWED_HOSTS): for i, host in enumerate(ALLOWED_HOSTS):
if '://' in host: if '://' in host:
ALLOWED_HOSTS[i] = host.split('://')[1] ALLOWED_HOSTS[i] = host = host.split('://')[1]
if ':' in host:
ALLOWED_HOSTS[i] = host = host.split(':')[0]
# List of trusted origins for unsafe requests # List of trusted origins for unsafe requests
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins # Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins

View File

@ -85,7 +85,7 @@ for name, provider in providers.registry.provider_map.items():
cls cls
for cls in prov_mod.__dict__.values() for cls in prov_mod.__dict__.values()
if isinstance(cls, type) if isinstance(cls, type)
and not cls == OAuth2Adapter and cls != OAuth2Adapter
and issubclass(cls, OAuth2Adapter) and issubclass(cls, OAuth2Adapter)
] ]

View File

@ -27,7 +27,7 @@ def get_provider_app(provider):
return apps.first() return apps.first()
def check_provider(provider, raise_error=False): def check_provider(provider):
"""Check if the given provider is correctly configured. """Check if the given provider is correctly configured.
To be correctly configured, the following must be true: To be correctly configured, the following must be true:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -379,11 +379,11 @@ class BomItemResource(InvenTreeResource):
report_skipped = False report_skipped = False
clean_model_instances = True clean_model_instances = True
exclude = ['checksum', 'id', 'part', 'sub_part', 'validated'] exclude = ['checksum', 'part', 'sub_part', 'validated']
level = Field(attribute='level', column_name=_('BOM Level'), readonly=True) level = Field(attribute='level', column_name=_('BOM Level'), readonly=True)
bom_id = Field( id = Field(
attribute='pk', column_name=_('BOM Item ID'), widget=widgets.IntegerWidget() attribute='pk', column_name=_('BOM Item ID'), widget=widgets.IntegerWidget()
) )
@ -476,7 +476,6 @@ class BomItemResource(InvenTreeResource):
if is_importing: if is_importing:
to_remove += [ to_remove += [
'level', 'level',
'pk',
'part', 'part',
'part__IPN', 'part__IPN',
'part__name', 'part__name',

View File

@ -267,6 +267,8 @@ class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
except AttributeError: except AttributeError:
pass pass
kwargs.setdefault('context', self.get_serializer_context())
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):

View File

@ -30,7 +30,7 @@ class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
} }
TME_IS_QRCODE_REGEX = re.compile(r'([^\s:]+:[^\s:]+\s+)+(\S+(\s|$)+)+') TME_IS_QRCODE_REGEX = re.compile(r'([^\s:]+:[^\s:]+\s+)+(\S+(\s|$)+)+')
TME_IS_BARCODE2D_REGEX = re.compile(r'(([^\s]+)(\s+|$))+') TME_IS_OLD_BARCODE2D_REGEX = re.compile(r'(([^\s]+)(\s+|$))+')
# Custom field mapping # Custom field mapping
TME_QRCODE_FIELDS = { TME_QRCODE_FIELDS = {
@ -52,22 +52,19 @@ class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
key, value = item.split(':') key, value = item.split(':')
if key in self.TME_QRCODE_FIELDS: if key in self.TME_QRCODE_FIELDS:
barcode_fields[self.TME_QRCODE_FIELDS[key]] = value barcode_fields[self.TME_QRCODE_FIELDS[key]] = value
elif self.TME_IS_OLD_BARCODE2D_REGEX.fullmatch(barcode_data):
return barcode_fields # Old 2D Barcode format e.g. "PWBP-302 1PMPNWBP-302 Q1 K19361337/1"
elif self.TME_IS_BARCODE2D_REGEX.fullmatch(barcode_data):
# 2D Barcode format e.g. "PWBP-302 1PMPNWBP-302 Q1 K19361337/1"
for item in barcode_data.split(' '): for item in barcode_data.split(' '):
for k, v in self.ecia_field_map().items(): for k, v in self.ecia_field_map().items():
if item.startswith(k): if item.startswith(k):
barcode_fields[v] = item[len(k) :] barcode_fields[v] = item[len(k) :]
else: else:
return {} barcode_fields = self.parse_ecia_barcode2d(barcode_data)
# Custom handling for order number # Custom handling for order number
if SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER in barcode_fields: if SupplierBarcodeMixin.SUPPLIER_ORDER_NUMBER in barcode_fields:
order_number = barcode_fields[SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER] order_number = barcode_fields[SupplierBarcodeMixin.SUPPLIER_ORDER_NUMBER]
order_number = order_number.split('/')[0] order_number = order_number.split('/')[0]
barcode_fields[SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER] = order_number barcode_fields[SupplierBarcodeMixin.SUPPLIER_ORDER_NUMBER] = order_number
return barcode_fields return barcode_fields

View File

@ -13,12 +13,12 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='stockitemtestresult', model_name='stockitemtestresult',
name='finished_datetime', name='finished_datetime',
field=models.DateTimeField(blank=True, help_text='The timestamp of the test finish', verbose_name='Finished'), field=models.DateTimeField(blank=True, help_text='The timestamp of the test finish', null=True, verbose_name='Finished'),
), ),
migrations.AddField( migrations.AddField(
model_name='stockitemtestresult', model_name='stockitemtestresult',
name='started_datetime', name='started_datetime',
field=models.DateTimeField(blank=True, help_text='The timestamp of the test start', verbose_name='Started'), field=models.DateTimeField(blank=True, help_text='The timestamp of the test start', null=True, verbose_name='Started'),
), ),
migrations.AddField( migrations.AddField(
model_name='stockitemtestresult', model_name='stockitemtestresult',

View File

@ -11,7 +11,7 @@
[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7179/badge)](https://bestpractices.coreinfrastructure.org/projects/7179) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7179/badge)](https://bestpractices.coreinfrastructure.org/projects/7179)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/inventree/InvenTree/badge)](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/inventree/InvenTree/badge)](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
[![Netlify Status](https://api.netlify.com/api/v1/badges/9bbb2101-0a4d-41e7-ad56-b63fb6053094/deploy-status)](https://app.netlify.com/sites/inventree/deploys) [![Netlify Status](https://api.netlify.com/api/v1/badges/9bbb2101-0a4d-41e7-ad56-b63fb6053094/deploy-status)](https://app.netlify.com/sites/inventree/deploys)
[![DeepSource](https://app.deepsource.com/gh/inventree/InvenTree.svg/?label=active+issues&show_trend=false&token=trZWqixKLk2t-RXtpSIAslVJ)](https://app.deepsource.com/gh/inventree/InvenTree/) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=inventree_InvenTree&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=inventree_InvenTree)
[![Coveralls](https://img.shields.io/coveralls/github/inventree/InvenTree)](https://coveralls.io/github/inventree/InvenTree) [![Coveralls](https://img.shields.io/coveralls/github/inventree/InvenTree)](https://coveralls.io/github/inventree/InvenTree)
[![Crowdin](https://badges.crowdin.net/inventree/localized.svg)](https://crowdin.com/project/inventree) [![Crowdin](https://badges.crowdin.net/inventree/localized.svg)](https://crowdin.com/project/inventree)

View File

@ -90,7 +90,7 @@ function detect_envs() {
echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}" echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}"
# Install parser # Install parser
pip install jc -q pip install jc==1.25.2 -q
# Load config # Load config
local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml) local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)

View File

@ -45,6 +45,8 @@
root * /var/www/media root * /var/www/media
file_server file_server
header Content-Disposition attachment
forward_auth {$INVENTREE_SERVER:"http://inventree-server:8000"} { forward_auth {$INVENTREE_SERVER:"http://inventree-server:8000"} {
uri /auth/ uri /auth/
} }

View File

@ -48,7 +48,7 @@ pip==24.0
pip-tools==7.4.1 pip-tools==7.4.1
platformdirs==4.2.0 platformdirs==4.2.0
# via virtualenv # via virtualenv
pre-commit==3.6.2 pre-commit==3.7.0
pycparser==2.21 pycparser==2.21
# via cffi # via cffi
pyproject-hooks==1.0.0 pyproject-hooks==1.0.0

View File

@ -23,27 +23,27 @@
"@mantine/core": "<7", "@mantine/core": "<7",
"@mantine/dates": "<7", "@mantine/dates": "<7",
"@mantine/dropzone": "<7", "@mantine/dropzone": "<7",
"@mantine/form": "<7", "@mantine/form": "<8",
"@mantine/hooks": "<7", "@mantine/hooks": "<7",
"@mantine/modals": "<7", "@mantine/modals": "<7",
"@mantine/notifications": "<7", "@mantine/notifications": "<7",
"@naisutech/react-tree": "^3.1.0", "@naisutech/react-tree": "^3.1.0",
"@sentry/react": "^7.108.0", "@sentry/react": "^7.109.0",
"@tabler/icons-react": "^3.1.0", "@tabler/icons-react": "^3.1.0",
"@tanstack/react-query": "^5.28.8", "@tanstack/react-query": "^5.28.9",
"@uiw/codemirror-theme-vscode": "^4.21.25", "@uiw/codemirror-theme-vscode": "^4.21.25",
"@uiw/react-codemirror": "^4.21.25", "@uiw/react-codemirror": "^4.21.25",
"@uiw/react-split": "^5.9.3", "@uiw/react-split": "^5.9.3",
"axios": "^1.6.7", "axios": "^1.6.7",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"easymde": "^2.18.0", "easymde": "^2.18.0",
"embla-carousel-react": "^7.1.0", "embla-carousel-react": "^8.0.0",
"html5-qrcode": "^2.3.8", "html5-qrcode": "^2.3.8",
"mantine-datatable": "<7", "mantine-datatable": "<7",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-grid-layout": "^1.4.4", "react-grid-layout": "^1.4.4",
"react-hook-form": "^7.51.1", "react-hook-form": "^7.51.2",
"react-is": "^18.2.0", "react-is": "^18.2.0",
"react-router-dom": "^6.22.1", "react-router-dom": "^6.22.1",
"react-select": "^5.8.0", "react-select": "^5.8.0",
@ -58,15 +58,15 @@
"@lingui/cli": "^4.7.2", "@lingui/cli": "^4.7.2",
"@lingui/macro": "^4.7.2", "@lingui/macro": "^4.7.2",
"@playwright/test": "^1.41.2", "@playwright/test": "^1.41.2",
"@types/node": "^20.11.20", "@types/node": "^20.12.2",
"@types/react": "^18.2.71", "@types/react": "^18.2.73",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.23",
"@types/react-grid-layout": "^1.3.5", "@types/react-grid-layout": "^1.3.5",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.2.6", "vite": "^5.2.7",
"vite-plugin-babel-macros": "^1.0.6" "vite-plugin-babel-macros": "^1.0.6"
} }
} }

View File

@ -15,7 +15,7 @@ import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { IconCheck } from '@tabler/icons-react'; import { IconCheck } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -32,6 +32,7 @@ export function AuthenticationForm() {
const [classicLoginMode, setMode] = useDisclosure(true); const [classicLoginMode, setMode] = useDisclosure(true);
const [auth_settings] = useServerApiState((state) => [state.auth_settings]); const [auth_settings] = useServerApiState((state) => [state.auth_settings]);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false); const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false);
@ -52,7 +53,7 @@ export function AuthenticationForm() {
color: 'green', color: 'green',
icon: <IconCheck size="1rem" /> icon: <IconCheck size="1rem" />
}); });
navigate('/home'); navigate(location?.state?.redirectFrom ?? '/home');
} else { } else {
notifications.show({ notifications.show({
title: t`Login failed`, title: t`Login failed`,

View File

@ -64,6 +64,7 @@ export type ApiFormFieldType = {
| 'string' | 'string'
| 'boolean' | 'boolean'
| 'date' | 'date'
| 'datetime'
| 'integer' | 'integer'
| 'decimal' | 'decimal'
| 'float' | 'float'
@ -215,6 +216,7 @@ export function ApiFormField({
/> />
); );
case 'date': case 'date':
case 'datetime':
return <DateField controller={controller} definition={definition} />; return <DateField controller={controller} definition={definition} />;
case 'integer': case 'integer':
case 'decimal': case 'decimal':

View File

@ -1,9 +1,13 @@
import { DateInput } from '@mantine/dates'; import { DateInput } from '@mantine/dates';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import { useCallback, useId, useMemo } from 'react'; import { useCallback, useId, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form'; import { FieldValues, UseControllerReturn } from 'react-hook-form';
import { ApiFormFieldType } from './ApiFormField'; import { ApiFormFieldType } from './ApiFormField';
dayjs.extend(customParseFormat);
export default function DateField({ export default function DateField({
controller, controller,
definition definition
@ -18,13 +22,16 @@ export default function DateField({
fieldState: { error } fieldState: { error }
} = controller; } = controller;
const valueFormat =
definition.field_type == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss';
const onChange = useCallback( const onChange = useCallback(
(value: any) => { (value: any) => {
// Convert the returned date object to a string // Convert the returned date object to a string
if (value) { if (value) {
value = value.toString(); value = value.toString();
let date = new Date(value); let date = new Date(value);
value = date.toISOString().split('T')[0]; value = dayjs(value).format(valueFormat);
} }
field.onChange(value); field.onChange(value);
@ -50,7 +57,7 @@ export default function DateField({
value={dateValue} value={dateValue}
clearable={!definition.required} clearable={!definition.required}
onChange={onChange} onChange={onChange}
valueFormat="YYYY-MM-DD" valueFormat={valueFormat}
label={definition.label} label={definition.label}
description={definition.description} description={definition.description}
placeholder={definition.placeholder} placeholder={definition.placeholder}

View File

@ -1,5 +1,5 @@
import { Container, Flex, Space } from '@mantine/core'; import { Container, Flex, Space } from '@mantine/core';
import { Navigate, Outlet } from 'react-router-dom'; import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { InvenTreeStyle } from '../../globalStyle'; import { InvenTreeStyle } from '../../globalStyle';
import { useSessionState } from '../../states/SessionState'; import { useSessionState } from '../../states/SessionState';
@ -9,8 +9,12 @@ import { Header } from './Header';
export const ProtectedRoute = ({ children }: { children: JSX.Element }) => { export const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const [token] = useSessionState((state) => [state.token]); const [token] = useSessionState((state) => [state.token]);
const location = useLocation();
if (!token) { if (!token) {
return <Navigate to="/logged-in" replace />; return (
<Navigate to="/logged-in" state={{ redirectFrom: location.pathname }} />
);
} }
return children; return children;

View File

@ -88,6 +88,7 @@ export function formatPriceRange(
interface renderDateOptionsType { interface renderDateOptionsType {
showTime?: boolean; showTime?: boolean;
showSeconds?: boolean;
} }
/* /*
@ -106,6 +107,9 @@ export function renderDate(date: string, options: renderDateOptionsType = {}) {
if (options.showTime) { if (options.showTime) {
fmt += ' HH:mm'; fmt += ' HH:mm';
if (options.showSeconds) {
fmt += ':ss';
}
} }
const m = dayjs(date); const m = dayjs(date);

View File

@ -150,7 +150,9 @@ export function checkLoginState(
// Callback function when login fails // Callback function when login fails
const loginFailure = () => { const loginFailure = () => {
useSessionState.getState().clearToken(); useSessionState.getState().clearToken();
if (!no_redirect) navigate('/login'); if (!no_redirect) {
navigate('/login', { state: { redirectFrom: redirect } });
}
}; };
if (useSessionState.getState().hasToken()) { if (useSessionState.getState().hasToken()) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More