mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-10 02:36:59 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
@@ -18,6 +18,18 @@ jobs:
|
|||||||
- name: Check version number
|
- name: Check version number
|
||||||
run: |
|
run: |
|
||||||
python3 ci/check_version_number.py --dev
|
python3 ci/check_version_number.py --dev
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
cd docker
|
||||||
|
docker-compose build
|
||||||
|
docker-compose run inventree-dev-server invoke update
|
||||||
|
- name: Run unit tests
|
||||||
|
run: |
|
||||||
|
cd docker
|
||||||
|
docker-compose up -d
|
||||||
|
docker-compose run inventree-dev-server invoke wait
|
||||||
|
docker-compose run inventree-dev-server invoke test
|
||||||
|
docker-compose down
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# Test that the InvenTree docker image compiles correctly
|
|
||||||
|
|
||||||
# This CI action runs on pushes to either the master or stable branches
|
|
||||||
|
|
||||||
# 1. Build the development docker image (as per the documentation)
|
|
||||||
# 2. Install requied python libs into the docker container
|
|
||||||
# 3. Launch the container
|
|
||||||
# 4. Check that the API endpoint is available
|
|
||||||
|
|
||||||
name: Docker Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'master'
|
|
||||||
- 'stable'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Build Docker Image
|
|
||||||
run: |
|
|
||||||
cd docker
|
|
||||||
docker-compose -f docker-compose.sqlite.yml build
|
|
||||||
docker-compose -f docker-compose.sqlite.yml run inventree-dev-server invoke update
|
|
||||||
docker-compose -f docker-compose.sqlite.yml up -d
|
|
||||||
- name: Sleepy Time
|
|
||||||
run: sleep 60
|
|
||||||
- name: Test API
|
|
||||||
run: |
|
|
||||||
pip install requests
|
|
||||||
python3 ci/check_api_endpoint.py
|
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
Main JSON interface views
|
Main JSON interface views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Custom exception handling for the DRF API
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.debug import ExceptionReporter
|
||||||
|
|
||||||
|
from error_report.models import Error
|
||||||
|
|
||||||
|
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import serializers
|
||||||
|
import rest_framework.views as drfviews
|
||||||
|
|
||||||
|
|
||||||
|
def exception_handler(exc, context):
|
||||||
|
"""
|
||||||
|
Custom exception handler for DRF framework.
|
||||||
|
Ref: https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
|
||||||
|
|
||||||
|
Catches any errors not natively handled by DRF, and re-throws as an error DRF can handle
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = None
|
||||||
|
|
||||||
|
# Catch any django validation error, and re-throw a DRF validation error
|
||||||
|
if isinstance(exc, DjangoValidationError):
|
||||||
|
exc = DRFValidationError(detail=serializers.as_serializer_error(exc))
|
||||||
|
|
||||||
|
# Default to the built-in DRF exception handler
|
||||||
|
response = drfviews.exception_handler(exc, context)
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
# DRF handler did not provide a default response for this exception
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
error_detail = str(exc)
|
||||||
|
else:
|
||||||
|
error_detail = _("Error details can be found in the admin panel")
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'error': type(exc).__name__,
|
||||||
|
'error_class': str(type(exc)),
|
||||||
|
'detail': error_detail,
|
||||||
|
'path': context['request'].path,
|
||||||
|
'status_code': 500,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = Response(response_data, status=500)
|
||||||
|
|
||||||
|
# Log the exception to the database, too
|
||||||
|
kind, info, data = sys.exc_info()
|
||||||
|
|
||||||
|
Error.objects.create(
|
||||||
|
kind=kind.__name__,
|
||||||
|
info=info,
|
||||||
|
data='\n'.join(traceback.format_exception(kind, info, data)),
|
||||||
|
path=context['request'].path,
|
||||||
|
html=ExceptionReporter(context['request'], kind, info, data).get_traceback_html(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if response is not None:
|
||||||
|
# Convert errors returned under the label '__all__' to 'non_field_errors'
|
||||||
|
if '__all__' in response.data:
|
||||||
|
response.data['non_field_errors'] = response.data['__all__']
|
||||||
|
del response.data['__all__']
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
""" Custom fields used in InvenTree """
|
""" Custom fields used in InvenTree """
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .validators import allowable_url_schemes
|
from .validators import allowable_url_schemes
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
Helper forms which subclass Django forms to provide additional functionality
|
Helper forms which subclass Django forms to provide additional functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
Generic models which provide extra functionality over base Django model types.
|
Generic models which provide extra functionality over base Django model types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
import users.models
|
import users.models
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Serializers used in various InvenTree apps
|
Serializers used in various InvenTree apps
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tablib
|
import tablib
|
||||||
|
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ TEMPLATES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
|
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
|
||||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
|
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import warnings
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -11,6 +9,8 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
from django.core import mail as django_mail
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
@@ -52,6 +52,15 @@ def schedule_task(taskname, **kwargs):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def raise_warning(msg):
|
||||||
|
"""Log and raise a warning"""
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
|
# If testing is running raise a warning that can be asserted
|
||||||
|
if settings.TESTING:
|
||||||
|
warnings.warn(msg)
|
||||||
|
|
||||||
|
|
||||||
def offload_task(taskname, *args, force_sync=False, **kwargs):
|
def offload_task(taskname, *args, force_sync=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create an AsyncTask if workers are running.
|
Create an AsyncTask if workers are running.
|
||||||
@@ -67,28 +76,38 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
from InvenTree.status import is_worker_running
|
from InvenTree.status import is_worker_running
|
||||||
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
|
logger.warning(f"Could not offload task '{taskname}' - app registry not ready")
|
||||||
|
return
|
||||||
|
except (OperationalError, ProgrammingError): # pragma: no cover
|
||||||
|
raise_warning(f"Could not offload task '{taskname}' - database not ready")
|
||||||
|
|
||||||
if is_worker_running() and not force_sync: # pragma: no cover
|
if is_worker_running() and not force_sync: # pragma: no cover
|
||||||
# Running as asynchronous task
|
# Running as asynchronous task
|
||||||
try:
|
try:
|
||||||
task = AsyncTask(taskname, *args, **kwargs)
|
task = AsyncTask(taskname, *args, **kwargs)
|
||||||
task.run()
|
task.run()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
|
raise_warning(f"WARNING: '{taskname}' not started - Function not found")
|
||||||
|
else:
|
||||||
|
|
||||||
|
if callable(taskname):
|
||||||
|
# function was passed - use that
|
||||||
|
_func = taskname
|
||||||
else:
|
else:
|
||||||
# Split path
|
# Split path
|
||||||
try:
|
try:
|
||||||
app, mod, func = taskname.split('.')
|
app, mod, func = taskname.split('.')
|
||||||
app_mod = app + '.' + mod
|
app_mod = app + '.' + mod
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
raise_warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Import module from app
|
# Import module from app
|
||||||
try:
|
try:
|
||||||
_mod = importlib.import_module(app_mod)
|
_mod = importlib.import_module(app_mod)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
raise_warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Retrieve function
|
# Retrieve function
|
||||||
@@ -102,17 +121,11 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
if not _func:
|
if not _func:
|
||||||
_func = eval(func) # pragma: no cover
|
_func = eval(func) # pragma: no cover
|
||||||
except NameError:
|
except NameError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
raise_warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Workers are not running: run it as synchronous task
|
# Workers are not running: run it as synchronous task
|
||||||
_func(*args, **kwargs)
|
_func(*args, **kwargs)
|
||||||
|
|
||||||
except AppRegistryNotReady: # pragma: no cover
|
|
||||||
logger.warning(f"Could not offload task '{taskname}' - app registry not ready")
|
|
||||||
return
|
|
||||||
except (OperationalError, ProgrammingError): # pragma: no cover
|
|
||||||
logger.warning(f"Could not offload task '{taskname}' - database not ready")
|
|
||||||
|
|
||||||
|
|
||||||
def heartbeat():
|
def heartbeat():
|
||||||
@@ -205,25 +218,25 @@ def check_for_updates():
|
|||||||
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
|
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}') # pragma: no cover
|
||||||
|
|
||||||
data = json.loads(response.text)
|
data = json.loads(response.text)
|
||||||
|
|
||||||
tag = data.get('tag_name', None)
|
tag = data.get('tag_name', None)
|
||||||
|
|
||||||
if not tag:
|
if not tag:
|
||||||
raise ValueError("'tag_name' missing from GitHub response")
|
raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover
|
||||||
|
|
||||||
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
||||||
|
|
||||||
if len(match.groups()) != 3:
|
if len(match.groups()) != 3: # pragma: no cover
|
||||||
logger.warning(f"Version '{tag}' did not match expected pattern")
|
logger.warning(f"Version '{tag}' did not match expected pattern")
|
||||||
return
|
return
|
||||||
|
|
||||||
latest_version = [int(x) for x in match.groups()]
|
latest_version = [int(x) for x in match.groups()]
|
||||||
|
|
||||||
if len(latest_version) != 3:
|
if len(latest_version) != 3:
|
||||||
raise ValueError(f"Version '{tag}' is not correct format")
|
raise ValueError(f"Version '{tag}' is not correct format") # pragma: no cover
|
||||||
|
|
||||||
logger.info(f"Latest InvenTree version: '{tag}'")
|
logger.info(f"Latest InvenTree version: '{tag}'")
|
||||||
|
|
||||||
@@ -288,7 +301,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
|
|||||||
recipients = [recipients]
|
recipients = [recipients]
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
'django.core.mail.send_mail',
|
django_mail.send_mail,
|
||||||
subject,
|
subject,
|
||||||
body,
|
body,
|
||||||
from_email,
|
from_email,
|
||||||
|
|||||||
@@ -2,10 +2,20 @@
|
|||||||
Unit tests for task management
|
Unit tests for task management
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django_q.models import Schedule
|
from django_q.models import Schedule
|
||||||
|
|
||||||
|
from error_report.models import Error
|
||||||
|
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
|
||||||
|
threshold = timezone.now() - timedelta(days=30)
|
||||||
|
threshold_low = threshold - timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
class ScheduledTaskTests(TestCase):
|
class ScheduledTaskTests(TestCase):
|
||||||
@@ -41,3 +51,79 @@ class ScheduledTaskTests(TestCase):
|
|||||||
# But the 'minutes' should have been updated
|
# But the 'minutes' should have been updated
|
||||||
t = Schedule.objects.get(func=task)
|
t = Schedule.objects.get(func=task)
|
||||||
self.assertEqual(t.minutes, 5)
|
self.assertEqual(t.minutes, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def get_result():
|
||||||
|
"""Demo function for test_offloading"""
|
||||||
|
return 'abc'
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeTaskTests(TestCase):
|
||||||
|
"""Unit tests for tasks"""
|
||||||
|
|
||||||
|
def test_offloading(self):
|
||||||
|
"""Test task offloading"""
|
||||||
|
|
||||||
|
# Run with function ref
|
||||||
|
InvenTree.tasks.offload_task(get_result)
|
||||||
|
|
||||||
|
# Run with string ref
|
||||||
|
InvenTree.tasks.offload_task('InvenTree.test_tasks.get_result')
|
||||||
|
|
||||||
|
# Error runs
|
||||||
|
# Malformed taskname
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTree')
|
||||||
|
|
||||||
|
# Non exsistent app
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter')
|
||||||
|
|
||||||
|
# Non exsistent function
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist')
|
||||||
|
|
||||||
|
def test_task_hearbeat(self):
|
||||||
|
"""Test the task heartbeat"""
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat)
|
||||||
|
|
||||||
|
def test_task_delete_successful_tasks(self):
|
||||||
|
"""Test the task delete_successful_tasks"""
|
||||||
|
from django_q.models import Success
|
||||||
|
|
||||||
|
Success.objects.create(name='abc', func='abc', stopped=threshold, started=threshold_low)
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks)
|
||||||
|
results = Success.objects.filter(started__lte=threshold)
|
||||||
|
self.assertEqual(len(results), 0)
|
||||||
|
|
||||||
|
def test_task_delete_old_error_logs(self):
|
||||||
|
"""Test the task delete_old_error_logs"""
|
||||||
|
|
||||||
|
# Create error
|
||||||
|
error_obj = Error.objects.create()
|
||||||
|
error_obj.when = threshold_low
|
||||||
|
error_obj.save()
|
||||||
|
|
||||||
|
# Check that it is not empty
|
||||||
|
errors = Error.objects.filter(when__lte=threshold,)
|
||||||
|
self.assertNotEqual(len(errors), 0)
|
||||||
|
|
||||||
|
# Run action
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_error_logs)
|
||||||
|
|
||||||
|
# Check that it is empty again
|
||||||
|
errors = Error.objects.filter(when__lte=threshold,)
|
||||||
|
self.assertEqual(len(errors), 0)
|
||||||
|
|
||||||
|
def test_task_check_for_updates(self):
|
||||||
|
"""Test the task check_for_updates"""
|
||||||
|
# Check that setting should be empty
|
||||||
|
self.assertEqual(InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION'), '')
|
||||||
|
|
||||||
|
# Get new version
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates)
|
||||||
|
|
||||||
|
# Check that setting is not empty
|
||||||
|
response = InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION')
|
||||||
|
self.assertNotEqual(response, '')
|
||||||
|
self.assertTrue(bool(response))
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from test.support import EnvironmentVarGuard
|
import os
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
import django.core.exceptions as django_exceptions
|
import django.core.exceptions as django_exceptions
|
||||||
@@ -449,17 +451,20 @@ class TestSettings(TestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user_mdl = get_user_model()
|
self.user_mdl = get_user_model()
|
||||||
self.env = EnvironmentVarGuard()
|
|
||||||
|
|
||||||
# Create a user for auth
|
# Create a user for auth
|
||||||
user = get_user_model()
|
user = get_user_model()
|
||||||
self.user = user.objects.create_superuser('testuser1', 'test1@testing.com', 'password1')
|
self.user = user.objects.create_superuser('testuser1', 'test1@testing.com', 'password1')
|
||||||
self.client.login(username='testuser1', password='password1')
|
self.client.login(username='testuser1', password='password1')
|
||||||
|
|
||||||
def run_reload(self):
|
def in_env_context(self, envs={}):
|
||||||
|
"""Patch the env to include the given dict"""
|
||||||
|
return mock.patch.dict(os.environ, envs)
|
||||||
|
|
||||||
|
def run_reload(self, envs={}):
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
with self.env:
|
with self.in_env_context(envs):
|
||||||
settings.USER_ADDED = False
|
settings.USER_ADDED = False
|
||||||
registry.reload_plugins()
|
registry.reload_plugins()
|
||||||
|
|
||||||
@@ -475,25 +480,28 @@ class TestSettings(TestCase):
|
|||||||
self.assertEqual(user_count(), 1)
|
self.assertEqual(user_count(), 1)
|
||||||
|
|
||||||
# not enough set
|
# not enough set
|
||||||
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
self.run_reload({
|
||||||
self.run_reload()
|
'INVENTREE_ADMIN_USER': 'admin'
|
||||||
|
})
|
||||||
self.assertEqual(user_count(), 1)
|
self.assertEqual(user_count(), 1)
|
||||||
|
|
||||||
# enough set
|
# enough set
|
||||||
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
self.run_reload({
|
||||||
self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email
|
'INVENTREE_ADMIN_USER': 'admin', # set username
|
||||||
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password
|
'INVENTREE_ADMIN_EMAIL': 'info@example.com', # set email
|
||||||
self.run_reload()
|
'INVENTREE_ADMIN_PASSWORD': 'password123' # set password
|
||||||
|
})
|
||||||
self.assertEqual(user_count(), 2)
|
self.assertEqual(user_count(), 2)
|
||||||
|
|
||||||
# create user manually
|
# create user manually
|
||||||
self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password')
|
self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||||
self.assertEqual(user_count(), 3)
|
self.assertEqual(user_count(), 3)
|
||||||
# check it will not be created again
|
# check it will not be created again
|
||||||
self.env.set('INVENTREE_ADMIN_USER', 'testuser')
|
self.run_reload({
|
||||||
self.env.set('INVENTREE_ADMIN_EMAIL', 'test@testing.com')
|
'INVENTREE_ADMIN_USER': 'testuser',
|
||||||
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password')
|
'INVENTREE_ADMIN_EMAIL': 'test@testing.com',
|
||||||
self.run_reload()
|
'INVENTREE_ADMIN_PASSWORD': 'password',
|
||||||
|
})
|
||||||
self.assertEqual(user_count(), 3)
|
self.assertEqual(user_count(), 3)
|
||||||
|
|
||||||
# make sure to clean up
|
# make sure to clean up
|
||||||
@@ -517,20 +525,30 @@ class TestSettings(TestCase):
|
|||||||
|
|
||||||
def test_helpers_cfg_file(self):
|
def test_helpers_cfg_file(self):
|
||||||
# normal run - not configured
|
# normal run - not configured
|
||||||
self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file())
|
|
||||||
|
valid = [
|
||||||
|
'inventree/config.yaml',
|
||||||
|
'inventree/dev/config.yaml',
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertTrue(any([opt in config.get_config_file().lower() for opt in valid]))
|
||||||
|
|
||||||
# with env set
|
# with env set
|
||||||
with self.env:
|
with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}):
|
||||||
self.env.set('INVENTREE_CONFIG_FILE', 'my_special_conf.yaml')
|
self.assertIn('inventree/inventree/my_special_conf.yaml', config.get_config_file().lower())
|
||||||
self.assertIn('InvenTree/InvenTree/my_special_conf.yaml', config.get_config_file())
|
|
||||||
|
|
||||||
def test_helpers_plugin_file(self):
|
def test_helpers_plugin_file(self):
|
||||||
# normal run - not configured
|
# normal run - not configured
|
||||||
self.assertIn('InvenTree/InvenTree/plugins.txt', config.get_plugin_file())
|
|
||||||
|
valid = [
|
||||||
|
'inventree/plugins.txt',
|
||||||
|
'inventree/dev/plugins.txt',
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertTrue(any([opt in config.get_plugin_file().lower() for opt in valid]))
|
||||||
|
|
||||||
# with env set
|
# with env set
|
||||||
with self.env:
|
with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}):
|
||||||
self.env.set('INVENTREE_PLUGIN_FILE', 'my_special_plugins.txt')
|
|
||||||
self.assertIn('my_special_plugins.txt', config.get_plugin_file())
|
self.assertIn('my_special_plugins.txt', config.get_plugin_file())
|
||||||
|
|
||||||
def test_helpers_setting(self):
|
def test_helpers_setting(self):
|
||||||
@@ -539,8 +557,7 @@ class TestSettings(TestCase):
|
|||||||
self.assertEqual(config.get_setting(TEST_ENV_NAME, None, '123!'), '123!')
|
self.assertEqual(config.get_setting(TEST_ENV_NAME, None, '123!'), '123!')
|
||||||
|
|
||||||
# with env set
|
# with env set
|
||||||
with self.env:
|
with self.in_env_context({TEST_ENV_NAME: '321'}):
|
||||||
self.env.set(TEST_ENV_NAME, '321')
|
|
||||||
self.assertEqual(config.get_setting(TEST_ENV_NAME, None), '321')
|
self.assertEqual(config.get_setting(TEST_ENV_NAME, None), '321')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ In particular these views provide base functionality for rendering Django forms
|
|||||||
as JSON objects and passing them to modal forms (using jQuery / bootstrap).
|
as JSON objects and passing them to modal forms (using jQuery / bootstrap).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -797,13 +795,9 @@ class CurrencyRefreshView(RedirectView):
|
|||||||
On a POST request we will attempt to refresh the exchange rates
|
On a POST request we will attempt to refresh the exchange rates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task, update_exchange_rates
|
||||||
|
|
||||||
# Define associated task from InvenTree.tasks list of methods
|
offload_task(update_exchange_rates, force_sync=True)
|
||||||
taskname = 'InvenTree.tasks.update_exchange_rates'
|
|
||||||
|
|
||||||
# Run it
|
|
||||||
offload_task(taskname, force_sync=True)
|
|
||||||
|
|
||||||
return redirect(reverse_lazy('settings'))
|
return redirect(reverse_lazy('settings'))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
JSON API for the Build app
|
JSON API for the Build app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.urls import include, re_path
|
from django.urls import include, re_path
|
||||||
|
|
||||||
from rest_framework import filters, generics
|
from rest_framework import filters, generics
|
||||||
@@ -285,6 +282,13 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
|
|||||||
API endpoint for deleting multiple build outputs
|
API endpoint for deleting multiple build outputs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
|
ctx['to_complete'] = False
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
queryset = Build.objects.none()
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
serializer_class = build.serializers.BuildOutputDeleteSerializer
|
serializer_class = build.serializers.BuildOutputDeleteSerializer
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
"""
|
|
||||||
Django Forms for interacting with Build objects
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
Build database model definitions
|
Build database model definitions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -1141,12 +1139,13 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Callback function to be executed after a Build instance is saved
|
Callback function to be executed after a Build instance is saved
|
||||||
"""
|
"""
|
||||||
|
from . import tasks as build_tasks
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
# A new Build has just been created
|
# A new Build has just been created
|
||||||
|
|
||||||
# Run checks on required parts
|
# Run checks on required parts
|
||||||
InvenTree.tasks.offload_task('build.tasks.check_build_stock', instance)
|
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
||||||
|
|
||||||
|
|
||||||
class BuildOrderAttachment(InvenTreeAttachment):
|
class BuildOrderAttachment(InvenTreeAttachment):
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
JSON serializers for Build API
|
JSON serializers for Build API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -202,7 +199,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_quantity(self, quantity):
|
def validate_quantity(self, quantity):
|
||||||
|
|
||||||
if quantity < 0:
|
if quantity <= 0:
|
||||||
raise ValidationError(_("Quantity must be greater than zero"))
|
raise ValidationError(_("Quantity must be greater than zero"))
|
||||||
|
|
||||||
part = self.get_part()
|
part = self.get_part()
|
||||||
@@ -212,7 +209,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
if part.trackable:
|
if part.trackable:
|
||||||
raise ValidationError(_("Integer quantity required for trackable parts"))
|
raise ValidationError(_("Integer quantity required for trackable parts"))
|
||||||
|
|
||||||
if part.has_trackable_parts():
|
if part.has_trackable_parts:
|
||||||
raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts"))
|
raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts"))
|
||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
@@ -235,7 +232,6 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
serial_numbers = serial_numbers.strip()
|
serial_numbers = serial_numbers.strip()
|
||||||
|
|
||||||
# TODO: Field level validation necessary here?
|
|
||||||
return serial_numbers
|
return serial_numbers
|
||||||
|
|
||||||
auto_allocate = serializers.BooleanField(
|
auto_allocate = serializers.BooleanField(
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|||||||
+209
-3
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -305,6 +302,215 @@ class BuildTest(BuildAPITest):
|
|||||||
|
|
||||||
self.assertEqual(bo.status, BuildStatus.CANCELLED)
|
self.assertEqual(bo.status, BuildStatus.CANCELLED)
|
||||||
|
|
||||||
|
def test_create_delete_output(self):
|
||||||
|
"""
|
||||||
|
Test that we can create and delete build outputs via the API
|
||||||
|
"""
|
||||||
|
|
||||||
|
bo = Build.objects.get(pk=1)
|
||||||
|
|
||||||
|
n_outputs = bo.output_count
|
||||||
|
|
||||||
|
create_url = reverse('api-build-output-create', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
# Attempt to create outputs with invalid data
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 'not a number',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('A valid number is required', str(response.data))
|
||||||
|
|
||||||
|
for q in [-100, -10.3, 0]:
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': q,
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
if q == 0:
|
||||||
|
self.assertIn('Quantity must be greater than zero', str(response.data))
|
||||||
|
else:
|
||||||
|
self.assertIn('Ensure this value is greater than or equal to 0', str(response.data))
|
||||||
|
|
||||||
|
# Mark the part being built as 'trackable' (requires integer quantity)
|
||||||
|
bo.part.trackable = True
|
||||||
|
bo.part.save()
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 12.3,
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('Integer quantity required for trackable parts', str(response.data))
|
||||||
|
|
||||||
|
# Erroneous serial numbers
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 5,
|
||||||
|
'serial_numbers': '1, 2, 3, 4, 5, 6',
|
||||||
|
'batch': 'my-batch',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('Number of unique serial numbers (6) must match quantity (5)', str(response.data))
|
||||||
|
|
||||||
|
# At this point, no new build outputs should have been created
|
||||||
|
self.assertEqual(n_outputs, bo.output_count)
|
||||||
|
|
||||||
|
# Now, create with *good* data
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 5,
|
||||||
|
'serial_numbers': '1, 2, 3, 4, 5',
|
||||||
|
'batch': 'my-batch',
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5 new outputs have been created
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
|
||||||
|
# Attempt to create with identical serial numbers
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 3,
|
||||||
|
'serial_numbers': '1-3',
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('The following serial numbers already exist : 1,2,3', str(response.data))
|
||||||
|
|
||||||
|
# Double check no new outputs have been created
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
|
||||||
|
# Now, let's delete each build output individually via the API
|
||||||
|
outputs = bo.build_outputs.all()
|
||||||
|
|
||||||
|
delete_url = reverse('api-build-output-delete', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
delete_url,
|
||||||
|
{
|
||||||
|
'outputs': [],
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('A list of build outputs must be provided', str(response.data))
|
||||||
|
|
||||||
|
# Mark 1 build output as complete
|
||||||
|
bo.complete_build_output(outputs[0], self.user)
|
||||||
|
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
self.assertEqual(1, bo.complete_count)
|
||||||
|
|
||||||
|
# Delete all outputs at once
|
||||||
|
# Note: One has been completed, so this should fail!
|
||||||
|
response = self.post(
|
||||||
|
delete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': output.pk,
|
||||||
|
} for output in outputs
|
||||||
|
]
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('This build output has already been completed', str(response.data))
|
||||||
|
|
||||||
|
# No change to the build outputs
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
self.assertEqual(1, bo.complete_count)
|
||||||
|
|
||||||
|
# Let's delete 2 build outputs
|
||||||
|
response = self.post(
|
||||||
|
delete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': output.pk,
|
||||||
|
} for output in outputs[1:3]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
expected_code=201
|
||||||
|
)
|
||||||
|
|
||||||
|
# Two build outputs have been removed
|
||||||
|
self.assertEqual(n_outputs + 3, bo.output_count)
|
||||||
|
self.assertEqual(1, bo.complete_count)
|
||||||
|
|
||||||
|
# Tests for BuildOutputComplete serializer
|
||||||
|
complete_url = reverse('api-build-output-complete', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
# Let's mark the remaining outputs as complete
|
||||||
|
response = self.post(
|
||||||
|
complete_url,
|
||||||
|
{
|
||||||
|
'outputs': [],
|
||||||
|
'location': 4,
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('A list of build outputs must be provided', str(response.data))
|
||||||
|
|
||||||
|
for output in outputs[3:]:
|
||||||
|
output.refresh_from_db()
|
||||||
|
self.assertTrue(output.is_building)
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
complete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': output.pk
|
||||||
|
} for output in outputs[3:]
|
||||||
|
],
|
||||||
|
'location': 4,
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the outputs have been completed
|
||||||
|
self.assertEqual(3, bo.complete_count)
|
||||||
|
|
||||||
|
for output in outputs[3:]:
|
||||||
|
output.refresh_from_db()
|
||||||
|
self.assertEqual(output.location.pk, 4)
|
||||||
|
self.assertFalse(output.is_building)
|
||||||
|
|
||||||
|
# Try again, with an output which has already been completed
|
||||||
|
response = self.post(
|
||||||
|
complete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': outputs.last().pk,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('This build output has already been completed', str(response.data))
|
||||||
|
|
||||||
|
|
||||||
class BuildAllocationTest(BuildAPITest):
|
class BuildAllocationTest(BuildAPITest):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django views for interacting with Build objects
|
Django views for interacting with Build objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Provides a JSON API for common components.
|
Provides a JSON API for common components.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django forms for interacting with common objects
|
Django forms for interacting with common objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ Common database model definitions.
|
|||||||
These models are 'generic' and do not fit a particular business logic object.
|
These models are 'generic' and do not fit a particular business logic object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import decimal
|
import decimal
|
||||||
import math
|
import math
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
JSON serializers for common components
|
JSON serializers for common components
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
from InvenTree.helpers import get_objectreference
|
from InvenTree.helpers import get_objectreference
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
User-configurable settings for the common app
|
User-configurable settings for the common app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod, storage
|
from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod, storage
|
||||||
from plugin.models import NotificationUserSetting
|
from plugin.models import NotificationUserSetting
|
||||||
from part.test_part import BaseNotificationIntegrationTest
|
from part.test_part import BaseNotificationIntegrationTest
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from common.models import NotificationEntry
|
from common.models import NotificationEntry
|
||||||
|
from . import tasks as common_tasks
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
|
|
||||||
@@ -14,4 +15,4 @@ class TaskTest(TestCase):
|
|||||||
|
|
||||||
# check empty run
|
# check empty run
|
||||||
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
||||||
offload_task('common.tasks.delete_old_notifications',)
|
offload_task(common_tasks.delete_old_notifications,)
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
"""
|
"""
|
||||||
Unit tests for the views associated with the 'common' app
|
Unit tests for the views associated with the 'common' app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import json
|
import json
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django views for interacting with common models
|
Django views for interacting with common models
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Provides a JSON API for the Company app
|
Provides a JSON API for the Company app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django Forms for interacting with Company app
|
Django Forms for interacting with Company app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Company database model definitions
|
Company database model definitions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
""" Unit tests for Company views (see views.py) """
|
""" Unit tests for Company views (see views.py) """
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
Django views for interacting with Company app
|
Django views for interacting with Company app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -24,6 +21,7 @@ from plugin.registry import registry
|
|||||||
|
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
from plugin.base.label import label as plugin_label
|
||||||
|
|
||||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||||
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
|
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
|
||||||
@@ -156,7 +154,7 @@ class LabelPrintMixin:
|
|||||||
|
|
||||||
# Offload a background task to print the provided label
|
# Offload a background task to print the provided label
|
||||||
offload_task(
|
offload_task(
|
||||||
'plugin.base.label.label.print_label',
|
plugin_label.print_label,
|
||||||
plugin.plugin_slug(),
|
plugin.plugin_slug(),
|
||||||
image,
|
image,
|
||||||
label_instance=label_instance,
|
label_instance=label_instance,
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Label printing models
|
Label printing models
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# Tests for labels
|
# Tests for labels
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# Tests for labels
|
# Tests for labels
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1177
-1162
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1306
-1291
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1203
-1188
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
+1175
-1160
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
JSON API for the Order app
|
JSON API for the Order app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django Forms for interacting with Order objects
|
Django Forms for interacting with Order objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.2.13 on 2022-05-16 14:35
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0067_auto_20220516_1120'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='salesorderallocation',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1269,12 +1269,6 @@ class SalesOrderAllocation(models.Model):
|
|||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-so-allocation-list')
|
return reverse('api-so-allocation-list')
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = [
|
|
||||||
# Cannot allocate any given StockItem to the same line more than once
|
|
||||||
('line', 'item'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
Validate the SalesOrderAllocation object:
|
Validate the SalesOrderAllocation object:
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
JSON serializers for the Order API
|
JSON serializers for the Order API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -1287,14 +1284,18 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for entry in items:
|
for entry in items:
|
||||||
|
|
||||||
# Create a new SalesOrderAllocation
|
# Create a new SalesOrderAllocation
|
||||||
order.models.SalesOrderAllocation.objects.create(
|
allocation = order.models.SalesOrderAllocation(
|
||||||
line=entry.get('line_item'),
|
line=entry.get('line_item'),
|
||||||
item=entry.get('stock_item'),
|
item=entry.get('stock_item'),
|
||||||
quantity=entry.get('quantity'),
|
quantity=entry.get('quantity'),
|
||||||
shipment=shipment,
|
shipment=shipment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allocation.full_clean()
|
||||||
|
allocation.save()
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
||||||
""" Serializer for a SalesOrderExtraLine object """
|
""" Serializer for a SalesOrderExtraLine object """
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
""" Unit tests for Order views (see views.py) """
|
""" Unit tests for Order views (see views.py) """
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django views for interacting with Order app
|
Django views for interacting with Order app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Provides a JSON API for the Part app
|
Provides a JSON API for the Part app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django Forms for interacting with Part objects
|
Django Forms for interacting with Part objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
Part database model definitions
|
Part database model definitions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -61,7 +59,6 @@ from order import models as OrderModels
|
|||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
import part.settings as part_settings
|
import part.settings as part_settings
|
||||||
from stock import models as StockModels
|
from stock import models as StockModels
|
||||||
|
|
||||||
from plugin.models import MetadataMixin
|
from plugin.models import MetadataMixin
|
||||||
|
|
||||||
|
|
||||||
@@ -2293,12 +2290,13 @@ def after_save_part(sender, instance: Part, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Function to be executed after a Part is saved
|
Function to be executed after a Part is saved
|
||||||
"""
|
"""
|
||||||
|
from part import tasks as part_tasks
|
||||||
|
|
||||||
if not created and not InvenTree.ready.isImportingData():
|
if not created and not InvenTree.ready.isImportingData():
|
||||||
# Check part stock only if we are *updating* the part (not creating it)
|
# Check part stock only if we are *updating* the part (not creating it)
|
||||||
|
|
||||||
# Run this check in the background
|
# Run this check in the background
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance)
|
InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance)
|
||||||
|
|
||||||
|
|
||||||
class PartAttachment(InvenTreeAttachment):
|
class PartAttachment(InvenTreeAttachment):
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
User-configurable settings for the Part app
|
User-configurable settings for the Part app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -49,6 +46,6 @@ def notify_low_stock_if_required(part: part.models.Part):
|
|||||||
for p in parts:
|
for p in parts:
|
||||||
if p.is_part_low_on_stock():
|
if p.is_part_low_on_stock():
|
||||||
InvenTree.tasks.offload_task(
|
InvenTree.tasks.offload_task(
|
||||||
'part.tasks.notify_low_stock',
|
notify_low_stock,
|
||||||
p
|
p
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -589,32 +589,15 @@
|
|||||||
// Get a list of the selected BOM items
|
// Get a list of the selected BOM items
|
||||||
var rows = $("#bom-table").bootstrapTable('getSelections');
|
var rows = $("#bom-table").bootstrapTable('getSelections');
|
||||||
|
|
||||||
// TODO - In the future, display (in the dialog) which items are going to be deleted
|
if (rows.length == 0) {
|
||||||
|
rows = $('#bom-table').bootstrapTable('getData');
|
||||||
|
}
|
||||||
|
|
||||||
showQuestionDialog(
|
deleteBomItems(rows, {
|
||||||
'{% trans "Delete selected BOM items?" %}',
|
success: function() {
|
||||||
'{% trans "All selected BOM items will be deleted" %}',
|
$('#bom-table').bootstrapTable('refresh');
|
||||||
{
|
|
||||||
accept: function() {
|
|
||||||
|
|
||||||
// Keep track of each DELETE request
|
|
||||||
var requests = [];
|
|
||||||
|
|
||||||
rows.forEach(function(row) {
|
|
||||||
requests.push(
|
|
||||||
inventreeDelete(
|
|
||||||
`/api/bom/${row.pk}/`,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for *all* the requests to complete
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#bom-upload').click(function() {
|
$('#bom-upload').click(function() {
|
||||||
|
|||||||
+19
-17
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import PIL
|
import PIL
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -1049,24 +1046,29 @@ class PartDetailTests(InvenTreeAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('Upload a valid image', str(response.data))
|
||||||
|
|
||||||
# Now try to upload a valid image file
|
# Now try to upload a valid image file, in multiple formats
|
||||||
img = PIL.Image.new('RGB', (128, 128), color='red')
|
for fmt in ['jpg', 'png', 'bmp', 'webp']:
|
||||||
img.save('dummy_image.jpg')
|
fn = f'dummy_image.{fmt}'
|
||||||
|
|
||||||
with open('dummy_image.jpg', 'rb') as dummy_image:
|
img = PIL.Image.new('RGB', (128, 128), color='red')
|
||||||
response = upload_client.patch(
|
img.save(fn)
|
||||||
url,
|
|
||||||
{
|
|
||||||
'image': dummy_image,
|
|
||||||
},
|
|
||||||
format='multipart',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
with open(fn, 'rb') as dummy_image:
|
||||||
|
response = upload_client.patch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'image': dummy_image,
|
||||||
|
},
|
||||||
|
format='multipart',
|
||||||
|
)
|
||||||
|
|
||||||
# And now check that the image has been set
|
self.assertEqual(response.status_code, 200)
|
||||||
p = Part.objects.get(pk=pk)
|
|
||||||
|
# And now check that the image has been set
|
||||||
|
p = Part.objects.get(pk=pk)
|
||||||
|
self.assertIsNotNone(p.image)
|
||||||
|
|
||||||
def test_details(self):
|
def test_details(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# Tests for Part Parameters
|
# Tests for Part Parameters
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase, TransactionTestCase
|
from django.test import TestCase, TransactionTestCase
|
||||||
import django.core.exceptions as django_exceptions
|
import django.core.exceptions as django_exceptions
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# Tests for the Part model
|
# Tests for the Part model
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@@ -67,11 +65,21 @@ class TemplateTagTest(TestCase):
|
|||||||
|
|
||||||
def test_hash(self):
|
def test_hash(self):
|
||||||
result_hash = inventree_extras.inventree_commit_hash()
|
result_hash = inventree_extras.inventree_commit_hash()
|
||||||
self.assertGreater(len(result_hash), 5)
|
if settings.DOCKER:
|
||||||
|
# Testing inside docker environment *may* return an empty git commit hash
|
||||||
|
# In such a case, skip this check
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.assertGreater(len(result_hash), 5)
|
||||||
|
|
||||||
def test_date(self):
|
def test_date(self):
|
||||||
d = inventree_extras.inventree_commit_date()
|
d = inventree_extras.inventree_commit_date()
|
||||||
self.assertEqual(len(d.split('-')), 3)
|
if settings.DOCKER:
|
||||||
|
# Testing inside docker environment *may* return an empty git commit hash
|
||||||
|
# In such a case, skip this check
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.assertEqual(len(d.split('-')), 3)
|
||||||
|
|
||||||
def test_github(self):
|
def test_github(self):
|
||||||
self.assertIn('github.com', inventree_extras.inventree_github_url())
|
self.assertIn('github.com', inventree_extras.inventree_github_url())
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
Django views for interacting with Part app
|
Django views for interacting with Part app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
JSON API for the plugin app
|
JSON API for the plugin app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import include, re_path
|
from django.urls import include, re_path
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user