mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-29 06:14:26 +00:00
Merge branch 'inventree:master' into not-working-tests
This commit is contained in:
@@ -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,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -795,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
|
||||||
|
|||||||
@@ -282,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
|
|
||||||
@@ -1139,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):
|
||||||
|
|||||||
@@ -199,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()
|
||||||
@@ -209,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
|
||||||
@@ -232,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1284,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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -59,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
|
||||||
|
|
||||||
|
|
||||||
@@ -2291,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):
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 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.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def trigger_event(event, *args, **kwargs):
|
|||||||
logger.debug(f"Event triggered: '{event}'")
|
logger.debug(f"Event triggered: '{event}'")
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
'plugin.events.register_event',
|
register_event,
|
||||||
event,
|
event,
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
@@ -72,7 +72,7 @@ def register_event(event, *args, **kwargs):
|
|||||||
|
|
||||||
# Offload a separate task for each plugin
|
# Offload a separate task for each plugin
|
||||||
offload_task(
|
offload_task(
|
||||||
'plugin.events.process_event',
|
process_event,
|
||||||
slug,
|
slug,
|
||||||
event,
|
event,
|
||||||
*args,
|
*args,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
"""API for location plugins"""
|
"""API for location plugins"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.exceptions import ParseError, NotFound
|
from rest_framework.exceptions import ParseError, NotFound
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -56,7 +53,7 @@ class LocatePluginView(APIView):
|
|||||||
try:
|
try:
|
||||||
StockItem.objects.get(pk=item_pk)
|
StockItem.objects.get(pk=item_pk)
|
||||||
|
|
||||||
offload_task('plugin.registry.call_function', plugin, 'locate_stock_item', item_pk)
|
offload_task(registry.call_function, plugin, 'locate_stock_item', item_pk)
|
||||||
|
|
||||||
data['item'] = item_pk
|
data['item'] = item_pk
|
||||||
|
|
||||||
@@ -69,7 +66,7 @@ class LocatePluginView(APIView):
|
|||||||
try:
|
try:
|
||||||
StockLocation.objects.get(pk=location_pk)
|
StockLocation.objects.get(pk=location_pk)
|
||||||
|
|
||||||
offload_task('plugin.registry.call_function', plugin, 'locate_stock_location', location_pk)
|
offload_task(registry.call_function, plugin, 'locate_stock_location', location_pk)
|
||||||
|
|
||||||
data['location'] = location_pk
|
data['location'] = location_pk
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from plugin.models import NotificationUserSetting
|
from plugin.models import NotificationUserSetting
|
||||||
from part.test_part import BaseNotificationIntegrationTest
|
from part.test_part import BaseNotificationIntegrationTest
|
||||||
from plugin.builtin.integration.core_notifications import CoreNotificationsPlugin
|
from plugin.builtin.integration.core_notifications import CoreNotificationsPlugin
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|||||||
@@ -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 ReportSnippet, ReportAsset
|
from .models import ReportSnippet, ReportAsset
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# -*- 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.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
|
|||||||
@@ -1,5 +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,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|||||||
@@ -2020,10 +2020,11 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Function to be executed after a StockItem object is deleted
|
Function to be executed after a StockItem object is deleted
|
||||||
"""
|
"""
|
||||||
|
from part import tasks as part_tasks
|
||||||
|
|
||||||
if not InvenTree.ready.isImportingData():
|
if not InvenTree.ready.isImportingData():
|
||||||
# Run this check in the background
|
# Run this check in the background
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
|
@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
|
||||||
@@ -2031,10 +2032,11 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Hook function to be executed after StockItem object is saved/updated
|
Hook function to be executed after StockItem object is saved/updated
|
||||||
"""
|
"""
|
||||||
|
from part import tasks as part_tasks
|
||||||
|
|
||||||
if not InvenTree.ready.isImportingData():
|
if not InvenTree.ready.isImportingData():
|
||||||
# Run this check in the background
|
# Run this check in the background
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part)
|
||||||
|
|
||||||
|
|
||||||
class StockItemAttachment(InvenTreeAttachment):
|
class StockItemAttachment(InvenTreeAttachment):
|
||||||
|
|||||||
@@ -225,6 +225,20 @@ function showApiError(xhr, url) {
|
|||||||
default:
|
default:
|
||||||
title = '{% trans "Unhandled Error Code" %}';
|
title = '{% trans "Unhandled Error Code" %}';
|
||||||
message = `{% trans "Error code" %}: ${xhr.status}`;
|
message = `{% trans "Error code" %}: ${xhr.status}`;
|
||||||
|
|
||||||
|
var response = xhr.responseJSON;
|
||||||
|
|
||||||
|
// The server may have provided some extra information about this error
|
||||||
|
if (response) {
|
||||||
|
if (response.error) {
|
||||||
|
title = response.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.detail) {
|
||||||
|
message = response.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
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.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|||||||
@@ -8,11 +8,10 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||

|
[](https://coveralls.io/github/inventree/InvenTree)
|
||||||
[](https://crowdin.com/project/inventree)
|
[](https://crowdin.com/project/inventree)
|
||||||

|

|
||||||

|

|
||||||

|
|
||||||
[](https://hub.docker.com/r/inventree/inventree)
|
[](https://hub.docker.com/r/inventree/inventree)
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
Reference in New Issue
Block a user