2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-07-08 16:11:23 +00:00

Merge branch 'inventree:master' into matmair/issue6281

This commit is contained in:
Matthias Mair
2024-03-27 09:30:57 +01:00
committed by GitHub
144 changed files with 67007 additions and 51940 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ runs:
# Python installs
- name: Set up Python ${{ env.python_version }}
if: ${{ inputs.python == 'true' }}
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # pin@v5.0.0
with:
python-version: ${{ env.python_version }}
cache: pip
+4 -1
View File
@@ -9,6 +9,9 @@ on:
pull_request_target:
types: [ "labeled", "closed" ]
permissions:
contents: read
jobs:
backport:
name: Backport PR
@@ -22,7 +25,7 @@ jobs:
)
steps:
- name: Backport Action
uses: sqren/backport-github-action@f54e19901f2a57f8b82360f2490d47ee82ec82c6 # pin@v9.2.2
uses: sqren/backport-github-action@f7073a2287aefc1fa12685eb25a712ab5620445c # pin@v9.2.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
auto_backport_label_prefix: backport-to-
@@ -11,6 +11,9 @@ on:
env:
python_version: 3.9
permissions:
contents: read
jobs:
check:
+5 -5
View File
@@ -71,7 +71,7 @@ jobs:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
- name: Set Up Python ${{ env.python_version }}
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
with:
python-version: ${{ env.python_version }}
- name: Version Check
@@ -124,10 +124,10 @@ jobs:
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # pin@v3.0.0
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # pin@v3.0.0
uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # pin@v3.2.0
- name: Set up cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # pin@v3.1.2
uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 # pin@v3.4.0
- name: Check if Dockerhub login is required
id: docker_login
run: |
@@ -138,14 +138,14 @@ jobs:
fi
- name: Login to Dockerhub
if: github.event_name != 'pull_request' && steps.docker_login.outputs.skip_dockerhub_login != 'true'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # pin@v3.0.0
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # pin@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log into registry ghcr.io
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # pin@v3.0.0
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # pin@v3.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+8 -6
View File
@@ -22,6 +22,8 @@ env:
INVENTREE_BACKUP_DIR: ../test_inventree_backup
INVENTREE_SITE_URL: http://localhost:8000
permissions:
contents: read
jobs:
paths-filter:
name: Filter
@@ -82,7 +84,7 @@ jobs:
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
with:
python-version: ${{ env.python_version }}
cache: 'pip'
@@ -103,7 +105,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
with:
python-version: ${{ env.python_version }}
- name: Check Config
@@ -147,7 +149,7 @@ jobs:
- name: Export API Documentation
run: invoke schema --ignore-warnings --filename InvenTree/schema.yml
- name: Upload schema
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # pin@v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # pin@v4.3.1
with:
name: schema.yml
path: InvenTree/schema.yml
@@ -192,7 +194,7 @@ jobs:
repository: inventree/schema
token: ${{ secrets.SCHEMA_PAT }}
- name: Download schema artifact
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: schema.yml
- name: Move schema to correct location
@@ -486,7 +488,7 @@ jobs:
run: cd src/frontend && npx playwright install --with-deps
- name: Run Playwright tests
run: cd src/frontend && npx playwright test
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # pin@v3.1.3
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # pin@v4.3.1
if: always()
with:
name: playwright-report
@@ -512,7 +514,7 @@ jobs:
run: |
cd InvenTree/web/static
zip -r frontend-build.zip web/
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # pin@v3.1.3
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # pin@v4.3.1
with:
name: frontend-build
path: InvenTree/web/static/web
+3
View File
@@ -5,6 +5,9 @@ on:
release:
types: [ published ]
permissions:
contents: read
jobs:
stable:
+3 -3
View File
@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with:
results_file: results.sarif
results_format: sarif
@@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: SARIF file
path: results.sarif
@@ -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@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
sarif_file: results.sarif
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # pin@v8.0.0
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # pin@v9.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue seems stale. Please react to show this is still important.'
+3
View File
@@ -9,6 +9,9 @@ env:
python_version: 3.9
node_version: 18
permissions:
contents: write
jobs:
build:
+3 -3
View File
@@ -27,8 +27,8 @@ repos:
--fix,
--preview
]
- repo: https://github.com/matmair/ruff-pre-commit
rev: fac27ee349cbf0f0d71c1069854bfe371d1c62a1 # uv-0.1.23
- repo: https://github.com/astral-sh/uv-pre-commit
rev: v0.1.24
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
@@ -62,7 +62,7 @@ repos:
- "prettier@^2.4.1"
- "@trivago/prettier-plugin-sort-imports"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v9.0.0-beta.2"
rev: "v9.0.0-rc.0"
hooks:
- id: eslint
additional_dependencies:
+5 -5
View File
@@ -91,7 +91,7 @@ class VersionView(APIView):
})
class VersionSerializer(serializers.Serializer):
class VersionInformationSerializer(serializers.Serializer):
"""Serializer for a single version."""
version = serializers.CharField()
@@ -101,21 +101,21 @@ class VersionSerializer(serializers.Serializer):
latest = serializers.BooleanField()
class Meta:
"""Meta class for VersionSerializer."""
"""Meta class for VersionInformationSerializer."""
fields = ['version', 'date', 'gh', 'text', 'latest']
fields = '__all__'
class VersionApiSerializer(serializers.Serializer):
"""Serializer for the version api endpoint."""
VersionSerializer(many=True)
VersionInformationSerializer(many=True)
class VersionTextView(ListAPI):
"""Simple JSON endpoint for InvenTree version text."""
serializer_class = VersionSerializer
serializer_class = VersionInformationSerializer
permission_classes = [permissions.IsAdminUser]
+5 -1
View File
@@ -1,11 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 184
INVENTREE_API_VERSION = 185
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v185 - 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836
- Remove /plugin/activate endpoint
- Update docstrings and typing for various API endpoints (no functional changes)
v184 - 2024-03-17 : https://github.com/inventree/InvenTree/pull/10464
- Add additional fields for tests (start/end datetime, test station)
+8
View File
@@ -0,0 +1,8 @@
"""Helpers for file handling in InvenTree."""
from pathlib import Path
from django.conf import settings
TEMPLATES_DIR = Path(__file__).parent.parent
MEDIA_STORAGE_DIR = settings.MEDIA_ROOT
+33 -4
View File
@@ -9,14 +9,15 @@ import os
import os.path
import re
from decimal import Decimal, InvalidOperation
from typing import TypeVar
from pathlib import Path
from typing import TypeVar, Union
from wsgiref.util import FileWrapper
import django.utils.timezone as timezone
from django.conf import settings
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage
from django.core.files.storage import Storage, default_storage
from django.http import StreamingHttpResponse
from django.utils.translation import gettext_lazy as _
@@ -861,9 +862,37 @@ def hash_barcode(barcode_data):
return str(hash.hexdigest())
def hash_file(filename: str):
def hash_file(filename: Union[str, Path], storage: Union[Storage, None] = None):
"""Return the MD5 hash of a file."""
return hashlib.md5(open(filename, 'rb').read()).hexdigest()
content = (
open(filename, 'rb').read()
if storage is None
else storage.open(str(filename), 'rb').read()
)
return hashlib.md5(content).hexdigest()
def current_time(local=True):
"""Return the current date and time as a datetime object.
- If timezone support is active, returns a timezone aware time
- If timezone support is not active, returns a timezone naive time
Arguments:
local: Return the time in the local timezone, otherwise UTC (default = True)
"""
if settings.USE_TZ:
now = timezone.now()
now = to_local_time(now, target_tz=server_timezone() if local else 'UTC')
return now
else:
return datetime.datetime.now()
def current_date(local=True):
"""Return the current date."""
return current_time(local=local).date()
def server_timezone() -> str:
+26 -4
View File
@@ -949,6 +949,8 @@ USE_I18N = True
# It generates a *lot* of cruft in the logs
if not TESTING:
USE_TZ = True # pragma: no cover
else:
USE_TZ = False
DATE_INPUT_FORMATS = ['%Y-%m-%d']
@@ -1127,12 +1129,32 @@ SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX = ''
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting(
'INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3, typecast=int
)
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting(
'INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5, typecast=int
)
# allauth rate limiting: https://docs.allauth.org/en/latest/account/rate_limits.html
# The default login rate limit is "5/m/user,5/m/ip,5/m/key"
login_attempts = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5)
try:
login_attempts = int(login_attempts)
login_attempts = f'{login_attempts}/m/ip,{login_attempts}/m/key'
except ValueError:
pass
ACCOUNT_RATE_LIMITS = {'login_failed': login_attempts}
# Default protocol for login
ACCOUNT_DEFAULT_HTTP_PROTOCOL = get_setting(
'INVENTREE_LOGIN_DEFAULT_HTTP_PROTOCOL', 'login_default_protocol', 'http'
'INVENTREE_LOGIN_DEFAULT_HTTP_PROTOCOL', 'login_default_protocol', None
)
if ACCOUNT_DEFAULT_HTTP_PROTOCOL is None:
if SITE_URL and SITE_URL.startswith('https://'):
# auto-detect HTTPS prtoocol
ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https'
else:
# default to http
ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'http'
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_PREVENT_ENUMERATION = True
ACCOUNT_EMAIL_SUBJECT_PREFIX = EMAIL_SUBJECT_PREFIX
+9 -3
View File
@@ -74,7 +74,7 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
verbose_name = _("Build Order")
verbose_name_plural = _("Build Orders")
OVERDUE_FILTER = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
OVERDUE_FILTER = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=InvenTree.helpers.current_date())
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
@@ -121,6 +121,12 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
super().clean()
if common.models.InvenTreeSetting.get_setting('BUILDORDER_REQUIRE_RESPONSIBLE'):
if not self.responsible:
raise ValidationError({
'responsible': _('Responsible user or group must be specified')
})
# Prevent changing target part after creation
if self.has_field_changed('part'):
raise ValidationError({
@@ -540,7 +546,7 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
if self.incomplete_count > 0:
return
self.completion_date = datetime.now().date()
self.completion_date = InvenTree.helpers.current_date()
self.completed_by = user
self.status = BuildStatus.COMPLETE.value
self.save()
@@ -622,7 +628,7 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
output.delete()
# Date of 'completion' is the date the build was cancelled
self.completion_date = datetime.now().date()
self.completion_date = InvenTree.helpers.current_date()
self.completed_by = user
self.status = BuildStatus.CANCELLED.value
+3 -2
View File
@@ -1,6 +1,6 @@
"""Background task definitions for the BuildOrder app"""
from datetime import datetime, timedelta
from datetime import timedelta
from decimal import Decimal
import logging
@@ -14,6 +14,7 @@ from plugin.events import trigger_event
import common.notifications
import build.models
import InvenTree.email
import InvenTree.helpers
import InvenTree.helpers_model
import InvenTree.tasks
from InvenTree.status_codes import BuildStatusGroups
@@ -222,7 +223,7 @@ def check_overdue_build_orders():
- Look at the 'target_date' of any outstanding BuildOrder objects
- If the 'target_date' expired *yesterday* then the order is just out of date
"""
yesterday = datetime.now().date() - timedelta(days=1)
yesterday = InvenTree.helpers.current_date() - timedelta(days=1)
overdue_orders = build.models.Build.objects.filter(
target_date=yesterday,
+1 -1
View File
@@ -148,7 +148,7 @@ class CurrencyExchangeView(APIView):
response = {
'base_currency': common.models.InvenTreeSetting.get_setting(
'INVENTREE_DEFAULT_CURRENCY', 'USD'
'INVENTREE_DEFAULT_CURRENCY', backup_value='USD'
),
'exchange_rates': {},
'updated': updated,
+82 -22
View File
@@ -13,7 +13,7 @@ import math
import os
import re
import uuid
from datetime import datetime, timedelta, timezone
from datetime import timedelta, timezone
from enum import Enum
from secrets import compare_digest
from typing import Any, Callable, TypedDict, Union
@@ -190,6 +190,8 @@ class BaseInvenTreeSetting(models.Model):
SETTINGS: dict[str, SettingsKeyType] = {}
CHECK_SETTING_KEY = False
extra_unique_fields: list[str] = []
class Meta:
@@ -286,16 +288,16 @@ class BaseInvenTreeSetting(models.Model):
def save_to_cache(self):
"""Save this setting object to cache."""
ckey = self.cache_key
key = self.cache_key
# skip saving to cache if no pk is set
if self.pk is None:
return
logger.debug("Saving setting '%s' to cache", ckey)
logger.debug("Saving setting '%s' to cache", key)
try:
cache.set(ckey, self, timeout=3600)
cache.set(key, self, timeout=3600)
except Exception:
pass
@@ -559,8 +561,8 @@ class BaseInvenTreeSetting(models.Model):
# Unless otherwise specified, attempt to create the setting
create = kwargs.pop('create', True)
# Perform cache lookup by default
do_cache = kwargs.pop('cache', True)
# Specify if cache lookup should be performed
do_cache = kwargs.pop('cache', False)
# Prevent saving to the database during data import
if InvenTree.ready.isImportingData():
@@ -572,12 +574,12 @@ class BaseInvenTreeSetting(models.Model):
create = False
do_cache = False
ckey = cls.create_cache_key(key, **kwargs)
cache_key = cls.create_cache_key(key, **kwargs)
if do_cache:
try:
# First attempt to find the setting object in the cache
cached_setting = cache.get(ckey)
cached_setting = cache.get(cache_key)
if cached_setting is not None:
return cached_setting
@@ -635,6 +637,17 @@ class BaseInvenTreeSetting(models.Model):
If it does not exist, return the backup value (default = None)
"""
if (
cls.CHECK_SETTING_KEY
and key not in cls.SETTINGS
and not key.startswith('_')
):
logger.warning(
"get_setting: Setting key '%s' is not defined for class %s",
key,
str(cls),
)
# If no backup value is specified, attempt to retrieve a "default" value
if backup_value is None:
backup_value = cls.get_setting_default(key, **kwargs)
@@ -670,6 +683,17 @@ class BaseInvenTreeSetting(models.Model):
change_user: User object (must be staff member to update a core setting)
create: If True, create a new setting if the specified key does not exist.
"""
if (
cls.CHECK_SETTING_KEY
and key not in cls.SETTINGS
and not key.startswith('_')
):
logger.warning(
"set_setting: Setting key '%s' is not defined for class %s",
key,
str(cls),
)
if change_user is not None and not change_user.is_staff:
return
@@ -1199,6 +1223,8 @@ class InvenTreeSetting(BaseInvenTreeSetting):
SETTINGS: dict[str, InvenTreeSettingsKeyType]
CHECK_SETTING_KEY = True
class Meta:
"""Meta options for InvenTreeSetting."""
@@ -1694,7 +1720,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'STOCK_DELETE_DEPLETED_DEFAULT': {
'name': _('Delete Depleted Stock'),
'description': _(
'Determines default behaviour when a stock item is depleted'
'Determines default behavior when a stock item is depleted'
),
'default': True,
'validator': bool,
@@ -1766,6 +1792,20 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': 'BO-{ref:04d}',
'validator': build.validators.validate_build_order_reference_pattern,
},
'BUILDORDER_REQUIRE_RESPONSIBLE': {
'name': _('Require Responsible Owner'),
'description': _('A responsible owner must be assigned to each order'),
'default': False,
'validator': bool,
},
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS': {
'name': _('Block Until Tests Pass'),
'description': _(
'Prevent build outputs from being completed until all required tests pass'
),
'default': False,
'validator': bool,
},
'RETURNORDER_ENABLED': {
'name': _('Enable Return Orders'),
'description': _('Enable return order functionality in the user interface'),
@@ -1780,6 +1820,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': 'RMA-{ref:04d}',
'validator': order.validators.validate_return_order_reference_pattern,
},
'RETURNORDER_REQUIRE_RESPONSIBLE': {
'name': _('Require Responsible Owner'),
'description': _('A responsible owner must be assigned to each order'),
'default': False,
'validator': bool,
},
'RETURNORDER_EDIT_COMPLETED_ORDERS': {
'name': _('Edit Completed Return Orders'),
'description': _(
@@ -1796,6 +1842,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': 'SO-{ref:04d}',
'validator': order.validators.validate_sales_order_reference_pattern,
},
'SALESORDER_REQUIRE_RESPONSIBLE': {
'name': _('Require Responsible Owner'),
'description': _('A responsible owner must be assigned to each order'),
'default': False,
'validator': bool,
},
'SALESORDER_DEFAULT_SHIPMENT': {
'name': _('Sales Order Default Shipment'),
'description': _('Enable creation of default shipment with sales orders'),
@@ -1818,6 +1870,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': 'PO-{ref:04d}',
'validator': order.validators.validate_purchase_order_reference_pattern,
},
'PURCHASEORDER_REQUIRE_RESPONSIBLE': {
'name': _('Require Responsible Owner'),
'description': _('A responsible owner must be assigned to each order'),
'default': False,
'validator': bool,
},
'PURCHASEORDER_EDIT_COMPLETED_ORDERS': {
'name': _('Edit Completed Purchase Orders'),
'description': _(
@@ -2004,14 +2062,6 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': False,
'validator': bool,
},
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS': {
'name': _('Block Until Tests Pass'),
'description': _(
'Prevent build outputs from being completed until all required tests pass'
),
'default': False,
'validator': bool,
},
'TEST_STATION_DATA': {
'name': _('Enable Test Station Data'),
'description': _('Enable test station data collection for test results'),
@@ -2054,7 +2104,9 @@ def label_printer_options():
class InvenTreeUserSetting(BaseInvenTreeSetting):
"""An InvenTreeSetting object with a usercontext."""
"""An InvenTreeSetting object with a user context."""
CHECK_SETTING_KEY = True
class Meta:
"""Meta options for InvenTreeUserSetting."""
@@ -2093,7 +2145,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'validator': bool,
},
'HOMEPAGE_BOM_REQUIRES_VALIDATION': {
'name': _('Show unvalidated BOMs'),
'name': _('Show invalid BOMs'),
'description': _('Show BOMs that await validation on the homepage'),
'default': False,
'validator': bool,
@@ -2406,6 +2458,14 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'validator': [int],
'default': '',
},
'DEFAULT_LINE_LABEL_TEMPLATE': {
'name': _('Default build line label template'),
'description': _(
'The build line label template to be automatically selected'
),
'validator': [int],
'default': '',
},
'NOTIFICATION_ERROR_REPORT': {
'name': _('Receive error reports'),
'description': _('Receive notifications for system errors'),
@@ -2616,7 +2676,7 @@ class VerificationMethod(Enum):
class WebhookEndpoint(models.Model):
"""Defines a Webhook entdpoint.
"""Defines a Webhook endpoint.
Attributes:
endpoint_id: Path to the webhook,
@@ -2855,7 +2915,7 @@ class NotificationEntry(MetaMixin):
@classmethod
def check_recent(cls, key: str, uid: int, delta: timedelta):
"""Test if a particular notification has been sent in the specified time period."""
since = datetime.now().date() - delta
since = InvenTree.helpers.current_date() - delta
entries = cls.objects.filter(key=key, uid=uid, updated__gte=since)
@@ -2951,7 +3011,7 @@ class NewsFeedEntry(models.Model):
- published: Date of publishing of the news item
- author: Author of news item
- summary: Summary of the news items content
- read: Was this iteam already by a superuser?
- read: Was this item already by a superuser?
"""
feed_id = models.CharField(verbose_name=_('Id'), unique=True, max_length=250)
+1 -1
View File
@@ -63,7 +63,7 @@ class SettingsSerializer(InvenTreeModelSerializer):
typ = serializers.CharField(read_only=True)
def get_choices(self, obj):
def get_choices(self, obj) -> list:
"""Returns the choices available for a given item."""
results = []
+3 -2
View File
@@ -2,7 +2,7 @@
import logging
import os
from datetime import datetime, timedelta
from datetime import timedelta
from django.conf import settings
from django.core.exceptions import AppRegistryNotReady
@@ -12,6 +12,7 @@ from django.utils import timezone
import feedparser
import requests
import InvenTree.helpers
from InvenTree.helpers_model import getModelsWithMixin
from InvenTree.models import InvenTreeNotesMixin
from InvenTree.tasks import ScheduledTask, scheduled_task
@@ -107,7 +108,7 @@ def delete_old_notes_images():
note.delete()
note_classes = getModelsWithMixin(InvenTreeNotesMixin)
before = datetime.now() - timedelta(days=90)
before = InvenTree.helpers.current_date() - timedelta(days=90)
for note in NotesImage.objects.filter(date__lte=before):
# Find any images which are no longer referenced by a note
+2 -2
View File
@@ -82,7 +82,7 @@ class CompanyDetail(RetrieveUpdateDestroyAPI):
class CompanyAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
"""API endpoint for the CompanyAttachment model."""
"""API endpoint for listing, creating and bulk deleting a CompanyAttachment."""
queryset = CompanyAttachment.objects.all()
serializer_class = CompanyAttachmentSerializer
@@ -215,7 +215,7 @@ class ManufacturerPartDetail(RetrieveUpdateDestroyAPI):
class ManufacturerPartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
"""API endpoint for listing (and creating) a ManufacturerPartAttachment (file upload)."""
"""API endpoint for listing, creating and bulk deleting a ManufacturerPartAttachment (file upload)."""
queryset = ManufacturerPartAttachment.objects.all()
serializer_class = ManufacturerPartAttachmentSerializer
+1 -1
View File
@@ -901,7 +901,7 @@ class SupplierPart(
def update_available_quantity(self, quantity):
"""Update the available quantity for this SupplierPart."""
self.available = quantity
self.availability_updated = datetime.now()
self.availability_updated = InvenTree.helpers.current_time()
self.save()
@property
+11 -17
View File
@@ -1,11 +1,9 @@
"""Shared templating code."""
import logging
import os
import warnings
from pathlib import Path
from django.conf import settings
from django.core.exceptions import AppRegistryNotReady
from django.core.files.storage import default_storage
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
@@ -18,9 +16,6 @@ from InvenTree.config import ensure_dir
logger = logging.getLogger('inventree')
MEDIA_STORAGE_DIR = Path(settings.MEDIA_ROOT)
class TemplatingMixin:
"""Mixin that contains shared templating code."""
@@ -84,8 +79,7 @@ class TemplatingMixin:
# Create root dir for templates
src_dir = self.get_src_dir(ref_name)
dst_dir = MEDIA_STORAGE_DIR.joinpath(self.name, 'inventree', ref_name)
ensure_dir(dst_dir, default_storage)
ensure_dir(Path(self.name, 'inventree', ref_name), default_storage)
# Copy each template across (if required)
for entry in data:
@@ -94,29 +88,27 @@ class TemplatingMixin:
def create_template_file(self, model, src_dir, data, ref_name):
"""Ensure a label template is in place."""
# Destination filename
filename = os.path.join(self.name, 'inventree', ref_name, data['file'])
filename = Path(self.name, 'inventree', ref_name, data['file'])
src_file = src_dir.joinpath(data['file'])
dst_file = MEDIA_STORAGE_DIR.joinpath(filename)
do_copy = False
if not dst_file.exists():
if not default_storage.exists(filename):
logger.info("%s template '%s' is not present", self.name, filename)
do_copy = True
else:
# Check if the file contents are different
src_hash = InvenTree.helpers.hash_file(src_file)
dst_hash = InvenTree.helpers.hash_file(dst_file)
dst_hash = InvenTree.helpers.hash_file(filename, default_storage)
if src_hash != dst_hash:
logger.info("Hash differs for '%s'", filename)
do_copy = True
if do_copy:
logger.info("Copying %s template '%s'", self.name, dst_file)
logger.info("Copying %s template '%s'", self.name, filename)
# Ensure destination dir exists
dst_file.parent.mkdir(parents=True, exist_ok=True)
ensure_dir(filename.parent, default_storage)
# Copy file
default_storage.save(filename, src_file.open('rb'))
@@ -135,6 +127,8 @@ class TemplatingMixin:
logger.info("Creating entry for %s '%s'", model, data.get('name'))
try:
model.objects.create(**self.get_new_obj_data(data, filename))
except Exception:
logger.warning("Failed to create %s '%s'", self.name, data['name'])
model.objects.create(**self.get_new_obj_data(data, str(filename)))
except Exception as _e:
logger.warning(
"Failed to create %s '%s' with error '%s'", self.name, data['name'], _e
)
+3 -3
View File
@@ -1,6 +1,5 @@
"""Label printing models."""
import datetime
import logging
import os
import sys
@@ -15,6 +14,7 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _
import build.models
import InvenTree.helpers
import InvenTree.models
import part.models
import stock.models
@@ -228,8 +228,8 @@ class LabelTemplate(InvenTree.models.InvenTreeMetadataModel):
# Add "basic" context data which gets passed to every label
context['base_url'] = get_base_url(request=request)
context['date'] = datetime.datetime.now().date()
context['datetime'] = datetime.datetime.now()
context['date'] = InvenTree.helpers.current_date()
context['datetime'] = InvenTree.helpers.current_time()
context['request'] = request
context['user'] = request.user
context['width'] = self.width
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
+61 -62
View File
@@ -1,44 +1,43 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: inventree\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-19 11:11+1100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"POT-Creation-Date: 2024-03-19 01:26+0000\n"
"PO-Revision-Date: 2024-03-20 12:27\n"
"Last-Translator: \n"
"Language-Team: Latvian\n"
"Language: lv_LV\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : "
"2);\n"
"Plural-Forms: nplurals=3; plural=(n==0 ? 0 : n%10==1 && n%100!=11 ? 1 : 2);\n"
"X-Crowdin-Project: inventree\n"
"X-Crowdin-Project-ID: 452300\n"
"X-Crowdin-Language: lv\n"
"X-Crowdin-File: /[inventree.InvenTree] l10/InvenTree/locale/en/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 154\n"
#: InvenTree/api.py:198
msgid "API endpoint not found"
msgstr ""
msgstr "API galapunkts nav atrasts"
#: InvenTree/api.py:462
msgid "User does not have permission to view this model"
msgstr ""
msgstr "Lietotājam nav atļaujas, lai apskatītu šo modeli"
#: InvenTree/conversion.py:160
#, python-brace-format
msgid "Invalid unit provided ({unit})"
msgstr ""
msgstr "Norādīta nederīga vienība ({unit})"
#: InvenTree/conversion.py:177
msgid "No value provided"
msgstr ""
msgstr "Nav norādīta vērtība"
#: InvenTree/conversion.py:205
#, python-brace-format
msgid "Could not convert {original} to {unit}"
msgstr ""
msgstr "Nevarēja konvertēt {original} par {unit}"
#: InvenTree/conversion.py:207
msgid "Invalid quantity supplied"
@@ -55,7 +54,7 @@ msgstr ""
#: InvenTree/fields.py:140
msgid "Enter date"
msgstr ""
msgstr "Ievadiet datumu"
#: InvenTree/fields.py:209 InvenTree/models.py:1022 build/serializers.py:438
#: build/serializers.py:516 build/templates/build/sidebar.html:21
@@ -78,171 +77,171 @@ msgstr ""
#: templates/js/translated/sales_order.js:1982
#: templates/js/translated/stock.js:1533 templates/js/translated/stock.js:2427
msgid "Notes"
msgstr ""
msgstr "Piezīmes"
#: InvenTree/format.py:164
#, python-brace-format
msgid "Value '{name}' does not appear in pattern format"
msgstr ""
msgstr "Vērtība '{name}' neparādās vajadzīgajā formātā"
#: InvenTree/format.py:175
msgid "Provided value does not match required pattern: "
msgstr ""
msgstr "Norādītā vērtība neatbilst nepieciešamajam formātam: "
#: InvenTree/forms.py:128
msgid "Enter password"
msgstr ""
msgstr "Ievadiet paroli"
#: InvenTree/forms.py:129
msgid "Enter new password"
msgstr ""
msgstr "Ievadiet jaunu paroli"
#: InvenTree/forms.py:138
msgid "Confirm password"
msgstr ""
msgstr "Apstiprināt paroli"
#: InvenTree/forms.py:139
msgid "Confirm new password"
msgstr ""
msgstr "Apstiprināt jauno paroli"
#: InvenTree/forms.py:143
msgid "Old password"
msgstr ""
msgstr "Vecā parole"
#: InvenTree/forms.py:182
msgid "Email (again)"
msgstr ""
msgstr "E-pasts (vēlreiz)"
#: InvenTree/forms.py:186
msgid "Email address confirmation"
msgstr ""
msgstr "E-pasta adreses apstiprinājums"
#: InvenTree/forms.py:209
msgid "You must type the same email each time."
msgstr ""
msgstr "Katru reizi jāievada viena un tā pati e-pasta adrese."
#: InvenTree/forms.py:253 InvenTree/forms.py:261
msgid "The provided primary email address is not valid."
msgstr ""
msgstr "Norādītā primārā e-pasta adrese nav derīga."
#: InvenTree/forms.py:268
msgid "The provided email domain is not approved."
msgstr ""
msgstr "Norādītais e-pasta domēns nav apstiprināts."
#: InvenTree/forms.py:395
msgid "Registration is disabled."
msgstr ""
msgstr "Reģistrācija ir izslēgta."
#: InvenTree/helpers.py:528 order/models.py:529 order/models.py:731
msgid "Invalid quantity provided"
msgstr ""
msgstr "Norādītais daudzums nav derīgs"
#: InvenTree/helpers.py:536
msgid "Empty serial number string"
msgstr ""
msgstr "Tukša sērijas numura rinda"
#: InvenTree/helpers.py:565
msgid "Duplicate serial"
msgstr ""
msgstr "Atkārtojas sērijas numurs"
#: InvenTree/helpers.py:597 InvenTree/helpers.py:640
#, python-brace-format
msgid "Invalid group range: {group}"
msgstr ""
msgstr "Nederīgs grupas diapazons: {group}"
#: InvenTree/helpers.py:628
#, python-brace-format
msgid "Group range {group} exceeds allowed quantity ({expected_quantity})"
msgstr ""
msgstr "Grupas diapazons {group} pārsniedz pieļaujamo daudzumu ({expected_quantity})"
#: InvenTree/helpers.py:658 InvenTree/helpers.py:665 InvenTree/helpers.py:684
#, python-brace-format
msgid "Invalid group sequence: {group}"
msgstr ""
msgstr "Nederīga grupas secība: {group}"
#: InvenTree/helpers.py:694
msgid "No serial numbers found"
msgstr ""
msgstr "Netika atrasts neviens sērijas numurs"
#: InvenTree/helpers.py:699
msgid "Number of unique serial numbers ({len(serials)}) must match quantity ({expected_quantity})"
msgstr ""
msgstr "Unikālo sērijas numuru skaitam ({len(serials)}) jāatbilst daudzumam ({expected_quantity})"
#: InvenTree/helpers.py:817
msgid "Remove HTML tags from this value"
msgstr ""
msgstr "Noņemiet HTML tagus no šīs vērtības"
#: InvenTree/helpers_model.py:150
msgid "Connection error"
msgstr ""
msgstr "Savienojuma kļūda"
#: InvenTree/helpers_model.py:155 InvenTree/helpers_model.py:162
msgid "Server responded with invalid status code"
msgstr ""
msgstr "Serveris atbildēja ar nederīgu statusa kodu"
#: InvenTree/helpers_model.py:158
msgid "Exception occurred"
msgstr ""
msgstr "Radās izņēmums"
#: InvenTree/helpers_model.py:168
msgid "Server responded with invalid Content-Length value"
msgstr ""
msgstr "Serveris atbildēja ar nederīgu Content-Length vērtību"
#: InvenTree/helpers_model.py:171
msgid "Image size is too large"
msgstr ""
msgstr "Attēla izmērs ir pārāk liels"
#: InvenTree/helpers_model.py:183
msgid "Image download exceeded maximum size"
msgstr ""
msgstr "Attēla lejupielāde pārsniedz maksimālo izmēru"
#: InvenTree/helpers_model.py:188
msgid "Remote server returned empty response"
msgstr ""
msgstr "Attālais serveris atgrieza tukšu atbildi"
#: InvenTree/helpers_model.py:196
msgid "Supplied URL is not a valid image file"
msgstr ""
msgstr "Norādītajā URL nav derīgs attēla fails"
#: InvenTree/locales.py:18
msgid "Bulgarian"
msgstr ""
msgstr "Bulgāru"
#: InvenTree/locales.py:19
msgid "Czech"
msgstr ""
msgstr "Čehu"
#: InvenTree/locales.py:20
msgid "Danish"
msgstr ""
msgstr "Dāņu"
#: InvenTree/locales.py:21
msgid "German"
msgstr ""
msgstr "Vācu"
#: InvenTree/locales.py:22
msgid "Greek"
msgstr ""
msgstr "Grieķu"
#: InvenTree/locales.py:23
msgid "English"
msgstr ""
msgstr "Angļu"
#: InvenTree/locales.py:24
msgid "Spanish"
msgstr ""
msgstr "Spāņu"
#: InvenTree/locales.py:25
msgid "Spanish (Mexican)"
msgstr ""
msgstr "Spāņu (Meksikāņu)"
#: InvenTree/locales.py:26
msgid "Farsi / Persian"
msgstr ""
msgstr "Farsi / Persiešu"
#: InvenTree/locales.py:27
msgid "Finnish"
msgstr ""
msgstr "Somu"
#: InvenTree/locales.py:28
msgid "French"
@@ -4725,7 +4724,6 @@ msgstr ""
#: label/templates/label/stockitem/qr.html:21
#: label/templates/label/stocklocation/qr.html:20
#: templates/allauth_2fa/setup.html:18
#: web/static/web/assets/TemplateManagementPanel-e180d0d6.js:20
msgid "QR Code"
msgstr ""
@@ -14161,3 +14159,4 @@ msgstr ""
#: users/models.py:413
msgid "Permission to delete items"
msgstr ""
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
+3 -3
View File
@@ -629,7 +629,7 @@ class PurchaseOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
class SalesOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
"""API endpoint for listing (and creating) a SalesOrderAttachment (file upload)."""
"""API endpoint for listing, creating and bulk deleting a SalesOrderAttachment (file upload)."""
queryset = models.SalesOrderAttachment.objects.all()
serializer_class = serializers.SalesOrderAttachmentSerializer
@@ -1097,7 +1097,7 @@ class SalesOrderShipmentComplete(CreateAPI):
class PurchaseOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
"""API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)."""
"""API endpoint for listing, creating and bulk deleting) a PurchaseOrderAttachment (file upload)."""
queryset = models.PurchaseOrderAttachment.objects.all()
serializer_class = serializers.PurchaseOrderAttachmentSerializer
@@ -1363,7 +1363,7 @@ class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
class ReturnOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
"""API endpoint for listing (and creating) a ReturnOrderAttachment (file upload)."""
"""API endpoint for listing, creating and bulk deleting a ReturnOrderAttachment (file upload)."""
queryset = models.ReturnOrderAttachment.objects.all()
serializer_class = serializers.ReturnOrderAttachmentSerializer
+35 -20
View File
@@ -207,6 +207,8 @@ class Order(
responsible: User (or group) responsible for managing the order
"""
REQUIRE_RESPONSIBLE_SETTING = None
class Meta:
"""Metaclass options. Abstract ensures no database table is created."""
@@ -219,7 +221,7 @@ class Order(
"""
self.reference_int = self.rebuild_reference_field(self.reference)
if not self.creation_date:
self.creation_date = datetime.now().date()
self.creation_date = InvenTree.helpers.current_date()
super().save(*args, **kwargs)
@@ -227,6 +229,16 @@ class Order(
"""Custom clean method for the generic order class."""
super().clean()
# Check if a responsible owner is required for this order type
if self.REQUIRE_RESPONSIBLE_SETTING:
if common_models.InvenTreeSetting.get_setting(
self.REQUIRE_RESPONSIBLE_SETTING, backup_value=False
):
if not self.responsible:
raise ValidationError({
'responsible': _('Responsible user or group must be specified')
})
# Check that the referenced 'contact' matches the correct 'company'
if self.company and self.contact:
if self.contact.company != self.company:
@@ -240,7 +252,7 @@ class Order(
It requires any subclasses to implement the get_status_class() class method
"""
today = datetime.now().date()
today = InvenTree.helpers.current_date()
return (
Q(status__in=cls.get_status_class().OPEN)
& ~Q(target_date=None)
@@ -347,6 +359,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
target_date: Expected delivery target date for PurchaseOrder completion (optional)
"""
REFERENCE_PATTERN_SETTING = 'PURCHASEORDER_REFERENCE_PATTERN'
REQUIRE_RESPONSIBLE_SETTING = 'PURCHASEORDER_REQUIRE_RESPONSIBLE'
def get_absolute_url(self):
"""Get the 'web' URL for this order."""
if settings.ENABLE_CLASSIC_FRONTEND:
@@ -372,9 +387,6 @@ class PurchaseOrder(TotalPriceMixin, Order):
return defaults
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'PURCHASEORDER_REFERENCE_PATTERN'
@staticmethod
def filterByDate(queryset, min_date, max_date):
"""Filter by 'minimum and maximum date range'.
@@ -572,7 +584,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
"""
if self.is_pending:
self.status = PurchaseOrderStatus.PLACED.value
self.issue_date = datetime.now().date()
self.issue_date = InvenTree.helpers.current_date()
self.save()
trigger_event('purchaseorder.placed', id=self.pk)
@@ -592,7 +604,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
"""
if self.status == PurchaseOrderStatus.PLACED:
self.status = PurchaseOrderStatus.COMPLETE.value
self.complete_date = datetime.now().date()
self.complete_date = InvenTree.helpers.current_date()
self.save()
@@ -805,6 +817,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
class SalesOrder(TotalPriceMixin, Order):
"""A SalesOrder represents a list of goods shipped outwards to a customer."""
REFERENCE_PATTERN_SETTING = 'SALESORDER_REFERENCE_PATTERN'
REQUIRE_RESPONSIBLE_SETTING = 'SALESORDER_REQUIRE_RESPONSIBLE'
def get_absolute_url(self):
"""Get the 'web' URL for this order."""
if settings.ENABLE_CLASSIC_FRONTEND:
@@ -828,9 +843,6 @@ class SalesOrder(TotalPriceMixin, Order):
return defaults
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'SALESORDER_REFERENCE_PATTERN'
@staticmethod
def filterByDate(queryset, min_date, max_date):
"""Filter by "minimum and maximum date range".
@@ -1018,7 +1030,7 @@ class SalesOrder(TotalPriceMixin, Order):
"""Change this order from 'PENDING' to 'IN_PROGRESS'."""
if self.status == SalesOrderStatus.PENDING:
self.status = SalesOrderStatus.IN_PROGRESS.value
self.issue_date = datetime.now().date()
self.issue_date = InvenTree.helpers.current_date()
self.save()
trigger_event('salesorder.issued', id=self.pk)
@@ -1032,7 +1044,7 @@ class SalesOrder(TotalPriceMixin, Order):
self.status = SalesOrderStatus.SHIPPED.value
self.shipped_by = user
self.shipment_date = datetime.now()
self.shipment_date = InvenTree.helpers.current_date()
self.save()
@@ -1334,7 +1346,7 @@ class PurchaseOrderLineItem(OrderLineItem):
OVERDUE_FILTER = (
Q(received__lt=F('quantity'))
& ~Q(target_date=None)
& Q(target_date__lt=datetime.now().date())
& Q(target_date__lt=InvenTree.helpers.current_date())
)
@staticmethod
@@ -1493,7 +1505,7 @@ class SalesOrderLineItem(OrderLineItem):
OVERDUE_FILTER = (
Q(shipped__lt=F('quantity'))
& ~Q(target_date=None)
& Q(target_date__lt=datetime.now().date())
& Q(target_date__lt=InvenTree.helpers.current_date())
)
@staticmethod
@@ -1736,7 +1748,9 @@ class SalesOrderShipment(
allocation.complete_allocation(user)
# Update the "shipment" date
self.shipment_date = kwargs.get('shipment_date', datetime.now())
self.shipment_date = kwargs.get(
'shipment_date', InvenTree.helpers.current_date()
)
self.shipped_by = user
# Was a tracking number provided?
@@ -1943,6 +1957,9 @@ class ReturnOrder(TotalPriceMixin, Order):
status: The status of the order (refer to status_codes.ReturnOrderStatus)
"""
REFERENCE_PATTERN_SETTING = 'RETURNORDER_REFERENCE_PATTERN'
REQUIRE_RESPONSIBLE_SETTING = 'RETURNORDER_REQUIRE_RESPONSIBLE'
def get_absolute_url(self):
"""Get the 'web' URL for this order."""
if settings.ENABLE_CLASSIC_FRONTEND:
@@ -1968,8 +1985,6 @@ class ReturnOrder(TotalPriceMixin, Order):
return defaults
REFERENCE_PATTERN_SETTING = 'RETURNORDER_REFERENCE_PATTERN'
def __str__(self):
"""Render a string representation of this ReturnOrder."""
return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}"
@@ -2063,7 +2078,7 @@ class ReturnOrder(TotalPriceMixin, Order):
"""Complete this ReturnOrder (if not already completed)."""
if self.status == ReturnOrderStatus.IN_PROGRESS:
self.status = ReturnOrderStatus.COMPLETE.value
self.complete_date = datetime.now().date()
self.complete_date = InvenTree.helpers.current_date()
self.save()
trigger_event('returnorder.completed', id=self.pk)
@@ -2076,7 +2091,7 @@ class ReturnOrder(TotalPriceMixin, Order):
"""Issue this ReturnOrder (if currently pending)."""
if self.status == ReturnOrderStatus.PENDING:
self.status = ReturnOrderStatus.IN_PROGRESS.value
self.issue_date = datetime.now().date()
self.issue_date = InvenTree.helpers.current_date()
self.save()
trigger_event('returnorder.issued', id=self.pk)
@@ -2149,7 +2164,7 @@ class ReturnOrder(TotalPriceMixin, Order):
)
# Update the LineItem
line.received_date = datetime.now().date()
line.received_date = InvenTree.helpers.current_date()
line.save()
trigger_event('returnorder.received', id=self.pk)
+10 -3
View File
@@ -34,7 +34,13 @@ from company.serializers import (
ContactSerializer,
SupplierPartSerializer,
)
from InvenTree.helpers import extract_serial_numbers, hash_barcode, normalize, str2bool
from InvenTree.helpers import (
current_date,
extract_serial_numbers,
hash_barcode,
normalize,
str2bool,
)
from InvenTree.serializers import (
InvenTreeAttachmentSerializer,
InvenTreeCurrencySerializer,
@@ -1140,11 +1146,12 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
user = request.user
# Extract shipping date (defaults to today's date)
shipment_date = data.get('shipment_date', datetime.now())
now = current_date()
shipment_date = data.get('shipment_date', now)
if shipment_date is None:
# Shipment date should not be None - check above only
# checks if shipment_date exists in data
shipment_date = datetime.now()
shipment_date = now
shipment.complete_shipment(
user,
+31 -9
View File
@@ -13,6 +13,7 @@ from djmoney.money import Money
from icalendar import Calendar
from rest_framework import status
from common.models import InvenTreeSetting
from common.settings import currency_codes
from company.models import Company, SupplierPart, SupplierPriceBreak
from InvenTree.status_codes import (
@@ -27,6 +28,7 @@ from InvenTree.unit_test import InvenTreeAPITestCase
from order import models
from part.models import Part
from stock.models import StockItem
from users.models import Owner
class OrderTest(InvenTreeAPITestCase):
@@ -347,15 +349,35 @@ class PurchaseOrderTest(OrderTest):
"""Test that we can create a new PurchaseOrder via the API."""
self.assignRole('purchase_order.add')
self.post(
reverse('api-po-list'),
{
'reference': 'PO-12345678',
'supplier': 1,
'description': 'A test purchase order',
},
expected_code=201,
)
setting = 'PURCHASEORDER_REQUIRE_RESPONSIBLE'
url = reverse('api-po-list')
InvenTreeSetting.set_setting(setting, False)
data = {
'reference': 'PO-12345678',
'supplier': 1,
'description': 'A test purchase order',
}
self.post(url, data, expected_code=201)
# Check the 'responsible required' field
InvenTreeSetting.set_setting(setting, True)
data['reference'] = 'PO-12345679'
data['responsible'] = None
response = self.post(url, data, expected_code=400)
self.assertIn('Responsible user or group must be specified', str(response.data))
data['responsible'] = Owner.objects.first().pk
response = self.post(url, data, expected_code=201)
# Revert the setting to previous value
InvenTreeSetting.set_setting(setting, False)
def test_po_creation_date(self):
"""Test that we can create set the creation_date field of PurchaseOrder via the API."""
+11 -1
View File
@@ -10,6 +10,8 @@ from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as rest_filters
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import permissions, serializers, status
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
@@ -214,6 +216,7 @@ class CategoryFilter(rest_filters.FilterSet):
help_text=_('Exclude sub-categories under the specified category'),
)
@extend_schema_field(OpenApiTypes.INT)
def filter_exclude_tree(self, queryset, name, value):
"""Exclude all sub-categories under the specified category."""
# Exclude the specified category
@@ -406,7 +409,7 @@ class PartInternalPriceList(ListCreateAPI):
class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
"""API endpoint for listing (and creating) a PartAttachment (file upload)."""
"""API endpoint for listing, creating and bulk deleting a PartAttachment (file upload)."""
queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer
@@ -1003,6 +1006,7 @@ class PartFilter(rest_filters.FilterSet):
method='filter_convert_from',
)
@extend_schema_field(OpenApiTypes.INT)
def filter_convert_from(self, queryset, name, part):
"""Limit the queryset to valid conversion options for the specified part."""
conversion_options = part.get_conversion_options()
@@ -1017,6 +1021,7 @@ class PartFilter(rest_filters.FilterSet):
method='filter_exclude_tree',
)
@extend_schema_field(OpenApiTypes.INT)
def filter_exclude_tree(self, queryset, name, part):
"""Exclude all parts and variants 'down' from the specified part from the queryset."""
children = part.get_descendants(include_self=True)
@@ -1027,6 +1032,7 @@ class PartFilter(rest_filters.FilterSet):
label='Ancestor', queryset=Part.objects.all(), method='filter_ancestor'
)
@extend_schema_field(OpenApiTypes.INT)
def filter_ancestor(self, queryset, name, part):
"""Limit queryset to descendants of the specified ancestor part."""
descendants = part.get_descendants(include_self=False)
@@ -1044,6 +1050,7 @@ class PartFilter(rest_filters.FilterSet):
label='In BOM Of', queryset=Part.objects.all(), method='filter_in_bom'
)
@extend_schema_field(OpenApiTypes.INT)
def filter_in_bom(self, queryset, name, part):
"""Limit queryset to parts in the BOM for the specified part."""
bom_parts = part.get_parts_in_bom()
@@ -1528,6 +1535,7 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
queryset=Part.objects.all(), method='filter_part', label=_('Part')
)
@extend_schema_field(OpenApiTypes.INT)
def filter_part(self, queryset, name, part):
"""Filter queryset to include only PartParameterTemplates which are referenced by a part."""
parameters = PartParameter.objects.filter(part=part)
@@ -1541,6 +1549,7 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
label=_('Category'),
)
@extend_schema_field(OpenApiTypes.INT)
def filter_category(self, queryset, name, category):
"""Filter queryset to include only PartParameterTemplates which are referenced by parts in this category."""
cats = category.get_descendants(include_self=True)
@@ -1828,6 +1837,7 @@ class BomFilter(rest_filters.FilterSet):
queryset=Part.objects.all(), method='filter_uses', label=_('Uses')
)
@extend_schema_field(OpenApiTypes.INT)
def filter_uses(self, queryset, name, part):
"""Filter the queryset based on the specified part."""
return queryset.filter(part.get_used_in_bom_item_filter())
+3 -1
View File
@@ -25,7 +25,9 @@ def compile_full_name_template(*args, **kwargs):
global _part_full_name_template
global _part_full_name_template_string
template_string = InvenTreeSetting.get_setting('PART_NAME_FORMAT', '')
template_string = InvenTreeSetting.get_setting(
'PART_NAME_FORMAT', backup_value='', cache=True
)
# Skip if the template string has not changed
if (
+3 -2
View File
@@ -37,6 +37,7 @@ import common.models
import common.settings
import InvenTree.conversion
import InvenTree.fields
import InvenTree.helpers
import InvenTree.models
import InvenTree.ready
import InvenTree.tasks
@@ -1728,7 +1729,7 @@ class Part(
self.bom_checksum = self.get_bom_hash()
self.bom_checked_by = user
self.bom_checked_date = datetime.now().date()
self.bom_checked_date = InvenTree.helpers.current_date()
self.save()
@@ -2715,7 +2716,7 @@ class PartPricing(common.models.MetaMixin):
)
if days > 0:
date_threshold = datetime.now().date() - timedelta(days=days)
date_threshold = InvenTree.helpers.current_date() - timedelta(days=days)
items = items.filter(updated__gte=date_threshold)
for item in items:
+1 -1
View File
@@ -266,7 +266,7 @@ def generate_stocktake_report(**kwargs):
buffer = io.StringIO()
buffer.write(dataset.export('csv'))
today = datetime.now().date().isoformat()
today = InvenTree.helpers.current_date().isoformat()
filename = f'InvenTree_Stocktake_{today}.csv'
report_file = ContentFile(buffer.getvalue(), name=filename)
+1 -1
View File
@@ -491,7 +491,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
PartCategory.objects.rebuild()
with self.assertNumQueriesLessThan(10):
with self.assertNumQueriesLessThan(12):
response = self.get(reverse('api-part-category-tree'), expected_code=200)
self.assertEqual(len(response.data), PartCategory.objects.count())
-1
View File
@@ -466,7 +466,6 @@ plugin_api_urls = [
# Plugin management
path('reload/', PluginReload.as_view(), name='api-plugin-reload'),
path('install/', PluginInstall.as_view(), name='api-plugin-install'),
path('activate/', PluginActivate.as_view(), name='api-plugin-activate'),
# Registry status
path(
'status/',
@@ -112,7 +112,7 @@ class ScheduleMixin:
@property
def has_scheduled_tasks(self):
"""Are tasks defined for this plugin."""
return bool(self.scheduled_tasks)
return bool(self.get_scheduled_tasks())
def validate_scheduled_tasks(self):
"""Check that the provided scheduled tasks are valid."""
+7 -1
View File
@@ -138,7 +138,13 @@ class MixinBase:
if fnc_name is True:
return True
return getattr(self, fnc_name, True)
attr = getattr(self, fnc_name, True)
if callable(attr):
return attr()
else:
return attr
return False
def add_mixin(self, key: str, fnc_enabled=True, cls=None):
+22 -5
View File
@@ -90,12 +90,19 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
def test_plugin_activate(self):
"""Test the plugin activate."""
test_plg = self.plugin_confs.first()
assert test_plg is not None
def assert_plugin_active(self, active):
self.assertEqual(PluginConfig.objects.all().first().active, active)
plgs = PluginConfig.objects.all().first()
assert plgs is not None
self.assertEqual(plgs.active, active)
# Should not work - not a superuser
response = self.client.post(reverse('api-plugin-activate'), {}, follow=True)
response = self.client.post(
reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}),
{},
follow=True,
)
self.assertEqual(response.status_code, 403)
# Make user superuser
@@ -109,7 +116,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
# Activate plugin with detail url
assert_plugin_active(self, False)
response = self.client.patch(
reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.id}),
reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}),
{},
follow=True,
)
@@ -123,7 +130,9 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
# Activate plugin
assert_plugin_active(self, False)
response = self.client.patch(
reverse('api-plugin-activate'), {'pk': test_plg.pk}, follow=True
reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}),
{},
follow=True,
)
self.assertEqual(response.status_code, 200)
assert_plugin_active(self, True)
@@ -133,6 +142,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
url = reverse('admin:plugin_pluginconfig_changelist')
test_plg = self.plugin_confs.first()
assert test_plg is not None
# deactivate plugin
response = self.client.post(
url,
@@ -181,6 +192,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
"""Test the PluginConfig model."""
# check mixin registry
plg = self.plugin_confs.first()
assert plg is not None
mixin_dict = plg.mixins()
self.assertIn('base', mixin_dict)
self.assertDictContainsSubset(
@@ -190,6 +203,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
# check reload on save
with self.assertWarns(Warning) as cm:
plg_inactive = self.plugin_confs.filter(active=False).first()
assert plg_inactive is not None
plg_inactive.active = True
plg_inactive.save()
self.assertEqual(cm.warning.args[0], 'A reload was triggered')
@@ -208,7 +223,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
# Wrong with pk
with self.assertRaises(NotFound) as exc:
check_plugin(plugin_slug=None, plugin_pk='123')
check_plugin(plugin_slug=None, plugin_pk=123)
self.assertEqual(str(exc.exception.detail), "Plugin '123' not installed")
def test_plugin_settings(self):
@@ -219,6 +234,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
# Activate the 'sample' plugin via the API
cfg = PluginConfig.objects.filter(key='sample').first()
assert cfg is not None
url = reverse('api-plugin-detail-activate', kwargs={'pk': cfg.pk})
self.client.patch(url, {}, expected_code=200)
+3 -2
View File
@@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as _
import build.models
import common.models
import InvenTree.exceptions
import InvenTree.helpers
import InvenTree.models
import order.models
import part.models
@@ -250,8 +251,8 @@ class ReportTemplateBase(MetadataMixin, ReportBase):
context = self.get_context_data(request)
context['base_url'] = get_base_url(request=request)
context['date'] = datetime.datetime.now().date()
context['datetime'] = datetime.datetime.now()
context['date'] = InvenTree.helpers.current_date()
context['datetime'] = InvenTree.helpers.current_time()
context['page_size'] = self.get_report_size()
context['report_template'] = self
context['report_description'] = self.description
+3 -5
View File
@@ -3,7 +3,6 @@
import os
import shutil
from io import StringIO
from pathlib import Path
from django.conf import settings
from django.core.cache import cache
@@ -19,6 +18,7 @@ from PIL import Image
import report.models as report_models
from build.models import Build
from common.models import InvenTreeSetting, InvenTreeUserSetting
from InvenTree.files import MEDIA_STORAGE_DIR, TEMPLATES_DIR
from InvenTree.unit_test import InvenTreeAPITestCase
from report.templatetags import barcode as barcode_tags
from report.templatetags import report as report_tags
@@ -249,11 +249,9 @@ class ReportTest(InvenTreeAPITestCase):
def copyReportTemplate(self, filename, description):
"""Copy the provided report template into the required media directory."""
src_dir = Path(__file__).parent.joinpath('templates', 'report')
src_dir = TEMPLATES_DIR.joinpath('report', 'templates', 'report')
template_dir = os.path.join('report', 'inventree', self.model.getSubdir())
dst_dir = settings.MEDIA_ROOT.joinpath(template_dir)
dst_dir = MEDIA_STORAGE_DIR.joinpath(template_dir)
if not dst_dir.exists(): # pragma: no cover
dst_dir.mkdir(parents=True, exist_ok=True)
+23 -6
View File
@@ -11,12 +11,15 @@ from django.urls import include, path
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as rest_filters
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import status
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
import common.models
import common.settings
import InvenTree.helpers
import stock.serializers as StockSerializers
from build.models import Build
from build.serializers import BuildSerializer
@@ -480,9 +483,17 @@ class StockFilter(rest_filters.FilterSet):
# Relationship filters
manufacturer = rest_filters.ModelChoiceFilter(
label='Manufacturer',
queryset=Company.objects.filter(is_manufacturer=True),
field_name='manufacturer_part__manufacturer',
queryset=Company.objects.all(),
method='filter_manufacturer',
)
@extend_schema_field(OpenApiTypes.INT)
def filter_manufacturer(self, queryset, name, company):
"""Filter by manufacturer."""
return queryset.filter(
Q(is_manufacturer=True) & Q(manufacturer_part__manufacturer=company)
)
supplier = rest_filters.ModelChoiceFilter(
label='Supplier',
queryset=Company.objects.filter(is_supplier=True),
@@ -725,6 +736,7 @@ class StockFilter(rest_filters.FilterSet):
label='Ancestor', queryset=StockItem.objects.all(), method='filter_ancestor'
)
@extend_schema_field(OpenApiTypes.INT)
def filter_ancestor(self, queryset, name, ancestor):
"""Filter based on ancestor stock item."""
return queryset.filter(parent__in=ancestor.get_descendants(include_self=True))
@@ -735,6 +747,7 @@ class StockFilter(rest_filters.FilterSet):
method='filter_category',
)
@extend_schema_field(OpenApiTypes.INT)
def filter_category(self, queryset, name, category):
"""Filter based on part category."""
child_categories = category.get_descendants(include_self=True)
@@ -745,6 +758,7 @@ class StockFilter(rest_filters.FilterSet):
label=_('BOM Item'), queryset=BomItem.objects.all(), method='filter_bom_item'
)
@extend_schema_field(OpenApiTypes.INT)
def filter_bom_item(self, queryset, name, bom_item):
"""Filter based on BOM item."""
return queryset.filter(bom_item.get_stock_filter())
@@ -753,6 +767,7 @@ class StockFilter(rest_filters.FilterSet):
label=_('Part Tree'), queryset=Part.objects.all(), method='filter_part_tree'
)
@extend_schema_field(OpenApiTypes.INT)
def filter_part_tree(self, queryset, name, part_tree):
"""Filter based on part tree."""
return queryset.filter(part__tree_id=part_tree.tree_id)
@@ -761,6 +776,7 @@ class StockFilter(rest_filters.FilterSet):
label=_('Company'), queryset=Company.objects.all(), method='filter_company'
)
@extend_schema_field(OpenApiTypes.INT)
def filter_company(self, queryset, name, company):
"""Filter by company (either manufacturer or supplier)."""
return queryset.filter(
@@ -795,7 +811,7 @@ class StockFilter(rest_filters.FilterSet):
# No filtering, does not make sense
return queryset
stale_date = datetime.now().date() + timedelta(days=stale_days)
stale_date = InvenTree.helpers.current_date() + timedelta(days=stale_days)
stale_filter = (
StockItem.IN_STOCK_FILTER
& ~Q(expiry_date=None)
@@ -813,6 +829,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
- GET: Return a list of all StockItem objects (with optional query filters)
- POST: Create a new StockItem
- DELETE: Delete multiple StockItem objects
"""
serializer_class = StockSerializers.StockItemSerializer
@@ -890,7 +907,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
# An expiry date was *not* specified - try to infer it!
if expiry_date is None and part.default_expiry > 0:
data['expiry_date'] = datetime.now().date() + timedelta(
data['expiry_date'] = InvenTree.helpers.current_date() + timedelta(
days=part.default_expiry
)
@@ -1034,7 +1051,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
filedata = dataset.export(export_format)
filename = f'InvenTree_StockItems_{datetime.now().strftime("%d-%b-%Y")}.{export_format}'
filename = f'InvenTree_StockItems_{InvenTree.helpers.current_date().strftime("%d-%b-%Y")}.{export_format}'
return DownloadFile(filedata, filename)
@@ -1199,7 +1216,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
class StockAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
"""API endpoint for listing (and creating) a StockItemAttachment (file upload)."""
"""API endpoint for listing, creating and bulk deleting a StockItemAttachment (file upload)."""
queryset = StockItemAttachment.objects.all()
serializer_class = StockSerializers.StockItemAttachmentSerializer
@@ -1,6 +1,5 @@
# Generated by Django 3.2.23 on 2023-12-18 18:52
import datetime
from django.db import migrations, models
@@ -14,12 +13,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='stockitemtestresult',
name='finished_datetime',
field=models.DateTimeField(blank=True, default=datetime.datetime.now, help_text='The timestamp of the test finish', verbose_name='Finished'),
field=models.DateTimeField(blank=True, help_text='The timestamp of the test finish', verbose_name='Finished'),
),
migrations.AddField(
model_name='stockitemtestresult',
name='started_datetime',
field=models.DateTimeField(blank=True, default=datetime.datetime.now, help_text='The timestamp of the test start', verbose_name='Started'),
field=models.DateTimeField(blank=True, help_text='The timestamp of the test start', verbose_name='Started'),
),
migrations.AddField(
model_name='stockitemtestresult',
@@ -0,0 +1,23 @@
# Generated by Django 4.2.11 on 2024-03-27 04:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0109_add_additional_test_fields'),
]
operations = [
migrations.AlterField(
model_name='stockitemtestresult',
name='finished_datetime',
field=models.DateTimeField(blank=True, help_text='The timestamp of the test finish', null=True, verbose_name='Finished'),
),
migrations.AlterField(
model_name='stockitemtestresult',
name='started_datetime',
field=models.DateTimeField(blank=True, help_text='The timestamp of the test start', null=True, verbose_name='Started'),
),
]
+10 -10
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
import os
from datetime import datetime, timedelta
from datetime import timedelta
from decimal import Decimal, InvalidOperation
from django.conf import settings
@@ -319,9 +319,9 @@ def generate_batch_code():
'STOCK_BATCH_CODE_TEMPLATE', ''
)
now = datetime.now()
now = InvenTree.helpers.current_time()
# Pass context data through to the template randering.
# Pass context data through to the template rendering.
# The following context variables are available for custom batch code generation
context = {
'date': now,
@@ -412,7 +412,7 @@ class StockItem(
EXPIRED_FILTER = (
IN_STOCK_FILTER
& ~Q(expiry_date=None)
& Q(expiry_date__lt=datetime.now().date())
& Q(expiry_date__lt=InvenTree.helpers.current_date())
)
def update_serial_number(self):
@@ -1011,7 +1011,7 @@ class StockItem(
if not self.in_stock:
return False
today = datetime.now().date()
today = InvenTree.helpers.current_date()
stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
@@ -1036,7 +1036,7 @@ class StockItem(
if not self.in_stock:
return False
today = datetime.now().date()
today = InvenTree.helpers.current_date()
return self.expiry_date < today
@@ -1438,7 +1438,7 @@ class StockItem(
item=self,
tracking_type=entry_type.value,
user=user,
date=datetime.now(),
date=InvenTree.helpers.current_time(),
notes=notes,
deltas=deltas,
)
@@ -1962,7 +1962,7 @@ class StockItem(
if count < 0:
return False
self.stocktake_date = datetime.now().date()
self.stocktake_date = InvenTree.helpers.current_date()
self.stocktake_user = user
if self.updateQuantity(count):
@@ -2464,15 +2464,15 @@ class StockItemTestResult(InvenTree.models.InvenTreeMetadataModel):
)
started_datetime = models.DateTimeField(
default=datetime.now,
blank=True,
null=True,
verbose_name=_('Started'),
help_text=_('The timestamp of the test start'),
)
finished_datetime = models.DateTimeField(
default=datetime.now,
blank=True,
null=True,
verbose_name=_('Finished'),
help_text=_('The timestamp of the test finish'),
)
+2 -2
View File
@@ -345,7 +345,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
# Add flag to indicate if the StockItem is stale
stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
stale_date = datetime.now().date() + timedelta(days=stale_days)
stale_date = InvenTree.helpers.current_date() + timedelta(days=stale_days)
stale_filter = (
StockItem.IN_STOCK_FILTER
& ~Q(expiry_date=None)
@@ -826,7 +826,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
deltas = {'status': status}
now = datetime.now()
now = InvenTree.helpers.current_time()
# Instead of performing database updates for each item,
# perform bulk database updates (much more efficient)
+1 -1
View File
@@ -446,7 +446,7 @@ class StockLocationTest(StockAPITestCase):
StockLocation.objects.rebuild()
with self.assertNumQueriesLessThan(10):
with self.assertNumQueriesLessThan(12):
response = self.get(reverse('api-location-tree'), expected_code=200)
self.assertEqual(len(response.data), StockLocation.objects.count())
@@ -13,6 +13,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PATTERN" %}
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REQUIRE_RESPONSIBLE" %}
{% include "InvenTree/settings/setting.html" with key="PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS" %}
</tbody>
</table>
@@ -11,6 +11,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PATTERN" %}
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REQUIRE_RESPONSIBLE" %}
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_EDIT_COMPLETED_ORDERS" icon='fa-edit' %}
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_AUTO_COMPLETE" icon='fa-check-circle' %}
</tbody>
@@ -13,6 +13,7 @@
<tbody>
{% include "InvenTree/settings/setting.html" with key="RETURNORDER_ENABLED" icon="fa-check-circle" %}
{% include "InvenTree/settings/setting.html" with key="RETURNORDER_REFERENCE_PATTERN" %}
{% include "InvenTree/settings/setting.html" with key="RETURNORDER_REQUIRE_RESPONSIBLE" %}
{% include "InvenTree/settings/setting.html" with key="RETURNORDER_EDIT_COMPLETED_ORDERS" icon="fa-edit" %}
</tbody>
</table>
@@ -12,6 +12,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PATTERN" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REQUIRE_RESPONSIBLE" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_DEFAULT_SHIPMENT" icon="fa-truck-loading" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_EDIT_COMPLETED_ORDERS" icon='fa-edit' %}
</tbody>
+4 -2
View File
@@ -53,7 +53,7 @@ def default_token_expiry():
"""Generate an expiry date for a newly created token."""
# TODO: Custom value for default expiry timeout
# TODO: For now, tokens last for 1 year
return datetime.datetime.now().date() + datetime.timedelta(days=365)
return InvenTree.helpers.current_date() + datetime.timedelta(days=365)
class ApiToken(AuthToken, InvenTree.models.MetadataMixin):
@@ -163,7 +163,9 @@ class ApiToken(AuthToken, InvenTree.models.MetadataMixin):
@admin.display(boolean=True, description=_('Expired'))
def expired(self):
"""Test if this token has expired."""
return self.expiry is not None and self.expiry < datetime.datetime.now().date()
return (
self.expiry is not None and self.expiry < InvenTree.helpers.current_date()
)
@property
@admin.display(boolean=True, description=_('Active'))
+11 -12
View File
@@ -24,14 +24,20 @@ def spa_bundle(manifest_path: Union[str, Path] = '', app: str = 'web'):
return f'{settings.STATIC_URL}{app}/{file}'
if manifest_path == '':
manifest_path = Path(__file__).parent.parent.joinpath(
'static/web/manifest.json'
manifest = Path(__file__).parent.parent.joinpath(
'static/web/.vite/manifest.json'
)
manifest = Path(manifest_path)
else:
manifest = Path(manifest_path)
if not manifest.exists():
logger.error('Manifest file not found')
return
# Try old path for manifest file
manifest = Path(__file__).parent.parent.joinpath('static/web/manifest.json')
# Final check - fail if manifest file not found
if not manifest.exists():
logger.error('Manifest file not found')
return
try:
manifest_data = json.load(manifest.open())
@@ -40,13 +46,6 @@ def spa_bundle(manifest_path: Union[str, Path] = '', app: str = 'web'):
return
return_string = ''
# CSS (based on index.css file as entrypoint)
css_index = manifest_data.get('index.css')
if css_index:
return_string += (
f'<link rel="stylesheet" href="{get_url(css_index["file"])}" />'
)
# JS (based on index.html file as entrypoint)
index = manifest_data.get('index.html')
dynamic_files = index.get('dynamicImports', [])
+1 -4
View File
@@ -26,14 +26,11 @@ class TemplateTagTest(InvenTreeTestCase):
def test_spa_bundle(self):
"""Test the 'spa_bundle' template tag."""
resp = spa_helper.spa_bundle()
self.assertTrue(
resp.startswith('<link rel="stylesheet" href="/static/web/assets/index')
)
shipped_js = resp.split('<script type="module" src="')[1:]
self.assertTrue(len(shipped_js) > 0)
self.assertTrue(len(shipped_js) == 3)
manifest_file = Path(__file__).parent.joinpath('static/web/manifest.json')
manifest_file = Path(__file__).parent.joinpath('static/web/.vite/manifest.json')
# Try with removed manifest file
manifest_file.rename(manifest_file.with_suffix('.json.bak')) # Rename
resp = resp = spa_helper.spa_bundle()
+2 -2
View File
@@ -29,11 +29,11 @@ class PreferredSerializer(serializers.Serializer):
pui = serializers.SerializerMethodField(read_only=True)
cui = serializers.SerializerMethodField(read_only=True)
def get_pui(self, obj):
def get_pui(self, obj) -> bool:
"""Return true if preferred method is PUI."""
return obj['preferred_method'] == 'pui'
def get_cui(self, obj):
def get_cui(self, obj) -> bool:
"""Return true if preferred method is CUI."""
return obj['preferred_method'] == 'cui'
+1 -1
View File
@@ -9,7 +9,7 @@
[![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest)
![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
[![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)
[![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/)
+53
View File
@@ -0,0 +1,53 @@
The InvenTree project is committed to providing a secure and safe environment for all users. We know that many of our users rely on InvenTree to manage the inventory and manufacturing for their small and mid-size businesses, and we take that responsibility seriously.
To that end, we have implemented a number of security measures over the years, which we will outline in this document.
## Organisational measures
The InvenTree project is managed by a small team of developers, who are responsible for the ongoing development and maintenance of the software. Two geographically distributed users have administrative access to the InvenTree codebase. Merges are only done by one of these two users, the maintainer Oliver.
InvenTree is open-source, and we welcome contributions from the community. However, all contributions are reviewed and scrutinised before being merged into the codebase.
We provide a written [Security Policy](https://github.com/inventree/InvenTree/blob/master/SECURITY.md) in our main repo to ensure that all security issues are handled in a timely manner.
If we become aware of a security issue, we will take immediate action to address the issue, and will provide a public disclosure of the issue once it has been resolved. We support assigning CVEs to security issues where appropriate. Our past security advisories can be found [here](https://github.com/inventree/InvenTree/security/advisories).
## Technical measures
### Code hosting
The InvenTree project is hosted on GitHub, and we rely on the security measures provided by GitHub to help protect the integrity of the codebase.
Among those are:
- Short-lived tokens where possible
- Dependabot for automated dependency updates / alerts
- Integrated security reporting
- (Optional but encouraged) Two-factor authentication for user accounts
- (Optional but encouraged) Signed commits / actions
### Code style
We enforce style and security checks in our CI/CD pipeline, and we have several automated tests to ensure that the codebase is secure and functional.
Checks are run on every pull request, and we require that all checks pass before a pull request can be merged.
### Current versions
InvenTree is built using the Django framework, which has a strong focus on security. We follow best practices for Django development, and we are committed to keeping the codebase up-to-date with the latest security patches and within supported versions.
### Test coverage
We run coverage tests on our codebase to ensure that we have a high level of test coverage above 90%. This is public and can be found [here](https://coveralls.io/github/inventree/InvenTree).
### Pinning dependencies
We are pinning dependencies to specific versions - aiming for complete reproducibility of builds - wherever possible. Combined with continuous OSV checks, we are able to react quickly to security issues in our dependencies.
## Best practices
We follow most of GitHubs community best practices, check our compliance [here](https://github.com/inventree/InvenTree/community).
We also follow OpenSSF recommendations where applicable and take part in multiple of their security efforts:
- OSSF Best Practices, currently at a [level of passing](https://www.bestpractices.dev/de/projects/7179)
- OSSF Scorecard, running with each merge [check current state](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
+4 -1
View File
@@ -292,7 +292,10 @@ The login-experience can be altered with the following settings:
| --- | --- | --- | --- |
| INVENTREE_LOGIN_CONFIRM_DAYS | login_confirm_days | Duration for which confirmation links are valid | 3 |
| INVENTREE_LOGIN_ATTEMPTS | login_attempts | Count of allowed login attempts before blocking user | 5 |
| INVENTREE_LOGIN_DEFAULT_HTTP_PROTOCOL | login_default_protocol | Default protocol to use for login callbacks (e.g. using [SSO](#single-sign-on)) | http |
| INVENTREE_LOGIN_DEFAULT_HTTP_PROTOCOL | login_default_protocol | Default protocol to use for login callbacks (e.g. using [SSO](#single-sign-on)) | Uses the protocol specified in `INVENTREE_SITE_URL`, or defaults to *http* |
!!! tip "Default Protocol"
If you have specified `INVENTREE_SITE_URL`, the default protocol will be used from that setting. Otherwise, the default protocol will be *http*.
### Authentication Backends
+1
View File
@@ -84,6 +84,7 @@ nav:
- Credits: credits.md
- Privacy: privacy.md
- Release Notes: releases/release_notes.md
- Security: security.md
- Install:
- Introduction: start/intro.md
- Configuration: start/config.md
+6
View File
@@ -8,6 +8,12 @@
},
{
"pattern": "http://127.0.0.1"
},
{
"pattern": "https://twitter.com/inventreedb"
},
{
"pattern": "https://www.reddit.com/r/InvenTree/"
}
]
}
+10 -10
View File
@@ -18,7 +18,7 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@lingui/core": "^4.7.1",
"@lingui/react": "^4.7.1",
"@lingui/react": "^4.7.2",
"@mantine/carousel": "<7",
"@mantine/core": "<7",
"@mantine/dates": "<7",
@@ -28,11 +28,11 @@
"@mantine/modals": "<7",
"@mantine/notifications": "<7",
"@naisutech/react-tree": "^3.1.0",
"@sentry/react": "^7.102.1",
"@tabler/icons-react": "^2.47.0",
"@tanstack/react-query": "^5.24.1",
"@uiw/codemirror-theme-vscode": "^4.21.22",
"@uiw/react-codemirror": "^4.21.22",
"@sentry/react": "^7.108.0",
"@tabler/icons-react": "^3.1.0",
"@tanstack/react-query": "^5.28.8",
"@uiw/codemirror-theme-vscode": "^4.21.25",
"@uiw/react-codemirror": "^4.21.25",
"@uiw/react-split": "^5.9.3",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
@@ -55,18 +55,18 @@
"@babel/core": "^7.23.9",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@lingui/cli": "^4.7.1",
"@lingui/macro": "^4.7.1",
"@lingui/cli": "^4.7.2",
"@lingui/macro": "^4.7.2",
"@playwright/test": "^1.41.2",
"@types/node": "^20.11.20",
"@types/react": "^18.2.59",
"@types/react": "^18.2.71",
"@types/react-dom": "^18.2.19",
"@types/react-grid-layout": "^1.3.5",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.2.1",
"babel-plugin-macros": "^3.1.0",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vite": "^5.2.6",
"vite-plugin-babel-macros": "^1.0.6"
}
}
@@ -8,14 +8,16 @@ import {
createStyles,
useMantineTheme
} from '@mantine/core';
import { IconChevronDown, TablerIconsProps } from '@tabler/icons-react';
import { IconChevronDown } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react';
import { TablerIconType } from '../../functions/icons';
interface SplitButtonOption {
key: string;
name: string;
onClick: () => void;
icon: (props: TablerIconsProps) => JSX.Element;
icon: TablerIconType;
disabled?: boolean;
tooltip?: string;
}
@@ -53,7 +53,7 @@ export type DetailsField =
| StringDetailField
| BooleanField
| LinkDetailField
| ProgressBarfield
| ProgressBarField
| StatusField
);
@@ -85,7 +85,7 @@ type ExternalLinkField = {
external: true;
};
type ProgressBarfield = {
type ProgressBarField = {
type: 'progressbar';
progress: number;
total: number;
@@ -96,8 +96,6 @@ type StatusField = {
model: ModelType;
};
type FieldValueType = string | number | undefined;
type FieldProps = {
field_data: any;
field_value: string | number;
@@ -14,8 +14,8 @@ import {
IconAlertTriangle,
IconDeviceFloppy,
IconExclamationCircle,
IconRefresh,
TablerIconsProps
IconProps,
IconRefresh
} from '@tabler/icons-react';
import Split from '@uiw/react-split';
import React, {
@@ -28,6 +28,7 @@ import React, {
import { api } from '../../../App';
import { ModelType } from '../../../enums/ModelType';
import { TablerIconType } from '../../../functions/icons';
import { apiUrl } from '../../../states/ApiState';
import { TemplateI } from '../../../tables/settings/TemplateTable';
import { SplitButton } from '../../buttons/SplitButton';
@@ -47,7 +48,7 @@ export type EditorComponent = React.ForwardRefExoticComponent<
export type Editor = {
key: string;
name: string;
icon: (props: TablerIconsProps) => React.JSX.Element;
icon: TablerIconType;
component: EditorComponent;
};
@@ -66,7 +67,7 @@ export type PreviewAreaComponent = React.ForwardRefExoticComponent<
export type PreviewArea = {
key: string;
name: string;
icon: (props: TablerIconsProps) => React.JSX.Element;
icon: TablerIconType;
component: PreviewAreaComponent;
};
+41 -23
View File
@@ -6,7 +6,7 @@ import {
Paper,
Text
} from '@mantine/core';
import { Button, Group, Stack } from '@mantine/core';
import { Button, Divider, Group, Stack } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { useQuery } from '@tanstack/react-query';
@@ -104,7 +104,7 @@ export function OptionsApiForm({
[props.url, props.pk, props.pathParams]
);
const { data } = useQuery({
const optionsQuery = useQuery({
enabled: true,
queryKey: [
'form-options-data',
@@ -139,18 +139,12 @@ export function OptionsApiForm({
const formProps: ApiFormProps = useMemo(() => {
const _props = { ...props };
// This forcefully overrides initial data
// Currently, most modals do not get pre-loaded correctly
if (!data) {
_props.fields = undefined;
}
if (!_props.fields) return _props;
for (const [k, v] of Object.entries(_props.fields)) {
_props.fields[k] = constructField({
field: v,
definition: data?.[k]
definition: optionsQuery?.data?.[k]
});
// If the user has specified initial data, use that value here
@@ -162,16 +156,30 @@ export function OptionsApiForm({
}
return _props;
}, [data, props]);
}, [optionsQuery.data, props]);
return <ApiForm id={id} props={formProps} />;
return (
<ApiForm
id={id}
props={formProps}
optionsLoading={optionsQuery.isFetching}
/>
);
}
/**
* An ApiForm component is a modal form which is rendered dynamically,
* based on an API endpoint.
*/
export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
export function ApiForm({
id,
props,
optionsLoading
}: {
id: string;
props: ApiFormProps;
optionsLoading: boolean;
}) {
const defaultValues: FieldValues = useMemo(() => {
let defaultValuesMap = mapFields(props.fields ?? {}, (_path, field) => {
return field.value ?? field.default ?? undefined;
@@ -227,6 +235,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
try {
// Await API call
let response = await api.get(url);
// Define function to process API response
const processFields = (fields: ApiFormFieldSet, data: NestedDict) => {
const res: NestedDict = {};
@@ -387,9 +396,16 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
() =>
isFormLoading ||
initialDataQuery.isFetching ||
optionsLoading ||
isSubmitting ||
!props.fields,
[isFormLoading, initialDataQuery.isFetching, isSubmitting, props.fields]
[
isFormLoading,
initialDataQuery.isFetching,
isSubmitting,
props.fields,
optionsLoading
]
);
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(() => {
@@ -431,16 +447,17 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
)}
<FormProvider {...form}>
<Stack spacing="xs">
{Object.entries(props.fields ?? {}).map(
([fieldName, field]) => (
<ApiFormField
key={fieldName}
fieldName={fieldName}
definition={field}
control={form.control}
/>
)
)}
{!optionsLoading &&
Object.entries(props.fields ?? {}).map(
([fieldName, field]) => (
<ApiFormField
key={fieldName}
fieldName={fieldName}
definition={field}
control={form.control}
/>
)
)}
</Stack>
</FormProvider>
{props.postFormContent}
@@ -449,6 +466,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
</Paper>
{/* Footer with Action Buttons */}
<Divider />
<div>
<Group position="right">
{props.actions?.map((action, i) => (
+1 -1
View File
@@ -246,7 +246,7 @@ export function openModalApiForm(props: OpenApiFormProps) {
children: (
<Stack spacing={'xs'}>
<Divider />
<ApiForm id={modalId} props={props} />
<ApiForm id={modalId} props={props} optionsLoading={false} />
</Stack>
)
});
+15 -6
View File
@@ -77,7 +77,7 @@ import { IconArrowBigDownLineFilled } from '@tabler/icons-react';
import { IconTruckReturn } from '@tabler/icons-react';
import { IconInfoCircle } from '@tabler/icons-react';
import { IconCalendarTime } from '@tabler/icons-react';
import { TablerIconsProps } from '@tabler/icons-react';
import { Icon, IconProps } from '@tabler/icons-react';
import React from 'react';
const icons = {
@@ -104,6 +104,8 @@ const icons = {
variants: IconVersions,
allocations: IconBookmarks,
bom: IconListTree,
build: IconTools,
build_order: IconTools,
builds: IconTools,
used_in: IconStack2,
manufacturers: IconBuildingFactory2,
@@ -170,6 +172,7 @@ const icons = {
quantity: IconNumbers,
progress: IconProgressCheck,
reference: IconHash,
serial: IconHash,
website: IconWorld,
email: IconMail,
phone: IconPhone,
@@ -184,6 +187,9 @@ const icons = {
};
export type InvenTreeIconType = keyof typeof icons;
export type TablerIconType = React.ForwardRefExoticComponent<
Omit<IconProps, 'ref'> & React.RefAttributes<Icon>
>;
/**
* Returns a Tabler Icon for the model field name supplied
@@ -193,13 +199,16 @@ export function GetIcon(field: InvenTreeIconType) {
return icons[field];
}
type IconProps = {
// Aliasing the new type name to make it distinct
type TablerIconProps = IconProps;
type InvenTreeIconProps = {
icon: InvenTreeIconType;
iconProps?: TablerIconsProps;
iconProps?: TablerIconProps;
};
export function InvenTreeIcon(props: IconProps) {
let Icon: (props: TablerIconsProps) => React.JSX.Element;
export function InvenTreeIcon(props: InvenTreeIconProps) {
let Icon: React.ForwardRefExoticComponent<React.RefAttributes<any>>;
if (props.icon in icons) {
Icon = GetIcon(props.icon);
@@ -212,6 +221,6 @@ export function InvenTreeIcon(props: IconProps) {
return <Icon {...props.iconProps} />;
}
function IconShapes(props: TablerIconsProps): Element {
function IconShapes(props: TablerIconProps): Element {
throw new Error('Function not implemented.');
}
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