Merge branch 'inventree:master' into matmair/issue6281
This commit is contained in:
commit
b769048538
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ on:
|
|||
release:
|
||||
types: [ published ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
stable:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ env:
|
|||
python_version: 3.9
|
||||
node_version: 18
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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', [])
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
[](https://inventree.readthedocs.io/en/latest/?badge=latest)
|
||||

|
||||
[](https://bestpractices.coreinfrastructure.org/projects/7179)
|
||||
[](https://securityscorecards.dev/viewer/?uri=github.com/inventree/inventree)
|
||||
[](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
|
||||
[](https://app.netlify.com/sites/inventree/deploys)
|
||||
[](https://app.deepsource.com/gh/inventree/InvenTree/)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
},
|
||||
{
|
||||
"pattern": "http://127.0.0.1"
|
||||
},
|
||||
{
|
||||
"pattern": "https://twitter.com/inventreedb"
|
||||
},
|
||||
{
|
||||
"pattern": "https://www.reddit.com/r/InvenTree/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue