Merge branch 'inventree:master' into not-working-tests

This commit is contained in:
Matthias Mair 2022-05-18 01:54:00 +02:00 committed by GitHub
commit 67733fa37b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 30275 additions and 29599 deletions

View File

@ -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

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from rest_framework.filters import OrderingFilter

View File

@ -1,7 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
from rest_framework import serializers

View File

@ -2,8 +2,6 @@
Generic models which provide extra functionality over base Django model types.
"""
from __future__ import unicode_literals
import re
import os
import logging

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from rest_framework import permissions
import users.models

View File

@ -353,7 +353,7 @@ TEMPLATES = [
]
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',

View File

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import json
import warnings
import requests
import logging
@ -11,6 +9,8 @@ from django.utils import timezone
from django.core.exceptions import AppRegistryNotReady
from django.db.utils import OperationalError, ProgrammingError
from django.core import mail as django_mail
from django.conf import settings
logger = logging.getLogger("inventree")
@ -52,6 +52,15 @@ def schedule_task(taskname, **kwargs):
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):
"""
Create an AsyncTask if workers are running.
@ -67,28 +76,38 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
import importlib
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
# Running as asynchronous task
try:
task = AsyncTask(taskname, *args, **kwargs)
task.run()
except ImportError:
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
if is_worker_running() and not force_sync: # pragma: no cover
# Running as asynchronous task
try:
task = AsyncTask(taskname, *args, **kwargs)
task.run()
except ImportError:
raise_warning(f"WARNING: '{taskname}' not started - Function not found")
else:
if callable(taskname):
# function was passed - use that
_func = taskname
else:
# Split path
try:
app, mod, func = taskname.split('.')
app_mod = app + '.' + mod
except ValueError:
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
raise_warning(f"WARNING: '{taskname}' not started - Malformed function path")
return
# Import module from app
try:
_mod = importlib.import_module(app_mod)
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
# Retrieve function
@ -102,17 +121,11 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
if not _func:
_func = eval(func) # pragma: no cover
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
# Workers are not running: run it as synchronous task
_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")
# Workers are not running: run it as synchronous task
_func(*args, **kwargs)
def heartbeat():
@ -205,25 +218,25 @@ def check_for_updates():
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
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)
tag = data.get('tag_name', None)
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)
if len(match.groups()) != 3:
if len(match.groups()) != 3: # pragma: no cover
logger.warning(f"Version '{tag}' did not match expected pattern")
return
latest_version = [int(x) for x in match.groups()]
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}'")
@ -288,7 +301,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
recipients = [recipients]
offload_task(
'django.core.mail.send_mail',
django_mail.send_mail,
subject,
body,
from_email,

View File

@ -2,10 +2,20 @@
Unit tests for task management
"""
from datetime import timedelta
from django.utils import timezone
from django.test import TestCase
from django_q.models import Schedule
from error_report.models import Error
import InvenTree.tasks
from common.models import InvenTreeSetting
threshold = timezone.now() - timedelta(days=30)
threshold_low = threshold - timedelta(days=1)
class ScheduledTaskTests(TestCase):
@ -41,3 +51,79 @@ class ScheduledTaskTests(TestCase):
# But the 'minutes' should have been updated
t = Schedule.objects.get(func=task)
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))

View File

@ -795,13 +795,9 @@ class CurrencyRefreshView(RedirectView):
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
taskname = 'InvenTree.tasks.update_exchange_rates'
# Run it
offload_task(taskname, force_sync=True)
offload_task(update_exchange_rates, force_sync=True)
return redirect(reverse_lazy('settings'))

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin

View File

@ -282,6 +282,13 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
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()
serializer_class = build.serializers.BuildOutputDeleteSerializer

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.apps import AppConfig

View File

@ -1,7 +0,0 @@
"""
Django Forms for interacting with Build objects
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

View File

@ -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
"""
from . import tasks as build_tasks
if created:
# A new Build has just been created
# 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):

View File

@ -199,7 +199,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
def validate_quantity(self, quantity):
if quantity < 0:
if quantity <= 0:
raise ValidationError(_("Quantity must be greater than zero"))
part = self.get_part()
@ -209,7 +209,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
if part.trackable:
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"))
return quantity
@ -232,7 +232,6 @@ class BuildOutputCreateSerializer(serializers.Serializer):
serial_numbers = serial_numbers.strip()
# TODO: Field level validation necessary here?
return serial_numbers
auto_allocate = serializers.BooleanField(

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
import logging

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime, timedelta
from django.urls import reverse
@ -305,6 +302,215 @@ class BuildTest(BuildAPITest):
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):
"""

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
from django.urls import reverse

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
from datetime import timedelta, datetime

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod, storage
from plugin.models import NotificationUserSetting
from part.test_part import BaseNotificationIntegrationTest

View File

@ -2,6 +2,7 @@
from django.test import TestCase
from common.models import NotificationEntry
from . import tasks as common_tasks
from InvenTree.tasks import offload_task
@ -14,4 +15,4 @@ class TaskTest(TestCase):
# check empty run
self.assertEqual(NotificationEntry.objects.all().count(), 0)
offload_task('common.tasks.delete_old_notifications',)
offload_task(common_tasks.delete_old_notifications,)

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from http import HTTPStatus
import json
from datetime import timedelta

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.apps import AppConfig

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
from django.core.exceptions import ValidationError

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from .models import StockItemLabel, StockLocationLabel, PartLabel

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from io import BytesIO
from PIL import Image
@ -24,6 +21,7 @@ from plugin.registry import registry
from stock.models import StockItem, StockLocation
from part.models import Part
from plugin.base.label import label as plugin_label
from .models import StockItemLabel, StockLocationLabel, PartLabel
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
@ -156,7 +154,7 @@ class LabelPrintMixin:
# Offload a background task to print the provided label
offload_task(
'plugin.base.label.label.print_label',
plugin_label.print_label,
plugin.plugin_slug(),
image,
label_instance=label_instance,

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin

View File

@ -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(),
),
]

View File

@ -1269,12 +1269,6 @@ class SalesOrderAllocation(models.Model):
def get_api_url():
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):
"""
Validate the SalesOrderAllocation object:

View File

@ -1284,14 +1284,18 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
with transaction.atomic():
for entry in items:
# Create a new SalesOrderAllocation
order.models.SalesOrderAllocation.objects.create(
allocation = order.models.SalesOrderAllocation(
line=entry.get('line_item'),
item=entry.get('stock_item'),
quantity=entry.get('quantity'),
shipment=shipment,
)
allocation.full_clean()
allocation.save()
class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
""" Serializer for a SalesOrderExtraLine object """

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
import logging
from django.db.utils import OperationalError, ProgrammingError

View File

@ -59,7 +59,6 @@ from order import models as OrderModels
from company.models import SupplierPart
import part.settings as part_settings
from stock import models as StockModels
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
"""
from part import tasks as part_tasks
if not created and not InvenTree.ready.isImportingData():
# Check part stock only if we are *updating* the part (not creating it)
# 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):

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
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:
if p.is_part_low_on_stock():
InvenTree.tasks.offload_task(
'part.tasks.notify_low_stock',
notify_low_stock,
p
)

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import PIL
from django.urls import reverse

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import transaction
from django.test import TestCase

View File

@ -1,8 +1,5 @@
# Tests for the Part model
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from allauth.account.models import EmailAddress
from django.contrib.auth import get_user_model

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging

View File

@ -37,7 +37,7 @@ def trigger_event(event, *args, **kwargs):
logger.debug(f"Event triggered: '{event}'")
offload_task(
'plugin.events.register_event',
register_event,
event,
*args,
**kwargs
@ -72,7 +72,7 @@ def register_event(event, *args, **kwargs):
# Offload a separate task for each plugin
offload_task(
'plugin.events.process_event',
process_event,
slug,
event,
*args,

View File

@ -1,8 +1,5 @@
"""API for location plugins"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from rest_framework import permissions
from rest_framework.exceptions import ParseError, NotFound
from rest_framework.response import Response
@ -56,7 +53,7 @@ class LocatePluginView(APIView):
try:
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
@ -69,7 +66,7 @@ class LocatePluginView(APIView):
try:
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

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from plugin.models import NotificationUserSetting
from part.test_part import BaseNotificationIntegrationTest
from plugin.builtin.integration.core_notifications import CoreNotificationsPlugin

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.urls import reverse

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from .models import ReportSnippet, ReportAsset

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import gettext_lazy as _
from django.urls import include, path, re_path

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import shutil

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin

View File

@ -1,4 +1,3 @@
from __future__ import unicode_literals
from django.apps import AppConfig

View File

@ -2020,10 +2020,11 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
"""
Function to be executed after a StockItem object is deleted
"""
from part import tasks as part_tasks
if not InvenTree.ready.isImportingData():
# 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')
@ -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
"""
from part import tasks as part_tasks
if not InvenTree.ready.isImportingData():
# 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):

View File

@ -225,6 +225,20 @@ function showApiError(xhr, url) {
default:
title = '{% trans "Unhandled Error Code" %}';
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;
}

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import gettext_lazy as _

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db.utils import OperationalError, ProgrammingError
from django.apps import AppConfig

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
from django.apps import apps
from django.urls import reverse

View File

@ -8,11 +8,10 @@
![CI](https://github.com/inventree/inventree/actions/workflows/qc_checks.yaml/badge.svg)
![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker_latest.yaml/badge.svg)
![Coveralls](https://img.shields.io/coveralls/github/inventree/InvenTree)
[![Coveralls](https://img.shields.io/coveralls/github/inventree/InvenTree)](https://coveralls.io/github/inventree/InvenTree)
[![Crowdin](https://badges.crowdin.net/inventree/localized.svg)](https://crowdin.com/project/inventree)
![Lines of code](https://img.shields.io/tokei/lines/github/inventree/InvenTree)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/inventree/inventree)
![PyPI - Downloads](https://img.shields.io/pypi/dm/inventree)
[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree)
![GitHub Org's stars](https://img.shields.io/github/stars/inventree?style=social)