Merge remote-tracking branch 'inventree/master' into shipment-assign-fix

This commit is contained in:
Oliver Walters 2022-05-17 18:48:57 +10:00
commit f4b470c396
105 changed files with 474 additions and 357 deletions

View File

@ -2,9 +2,6 @@
Main JSON interface views
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.http import JsonResponse

View File

@ -1,7 +1,5 @@
""" Custom fields used in InvenTree """
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
from .validators import allowable_url_schemes

View File

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

View File

@ -2,8 +2,6 @@
Helper forms which subclass Django forms to provide additional functionality
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from urllib.parse import urlencode
import logging

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

@ -2,9 +2,6 @@
Serializers used in various InvenTree apps
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import tablib

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,6 +76,11 @@ 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
@ -74,21 +88,26 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
task = AsyncTask(taskname, *args, **kwargs)
task.run()
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:
# 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,18 +121,12 @@ 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")
def heartbeat():
"""
@ -126,8 +139,8 @@ def heartbeat():
try:
from django_q.models import Success
logger.info("Could not perform heartbeat task - App registry not ready")
except AppRegistryNotReady: # pragma: no cover
logger.info("Could not perform heartbeat task - App registry not ready")
return
threshold = timezone.now() - timedelta(minutes=30)
@ -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

@ -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).
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import json
@ -797,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

@ -2,9 +2,6 @@
JSON API for the Build app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.urls import include, re_path
from rest_framework import filters, generics
@ -285,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

@ -2,8 +2,6 @@
Build database model definitions
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import decimal
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
"""
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

@ -2,9 +2,6 @@
JSON serializers for Build API
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import transaction
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import gettext_lazy as _
@ -202,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()
@ -212,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
@ -235,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

@ -2,9 +2,6 @@
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.views.generic import DetailView, ListView

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

@ -2,9 +2,6 @@
Provides a JSON API for common components.
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import json
from django.http.response import HttpResponse

View File

@ -2,9 +2,6 @@
Django forms for interacting with common objects
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import forms
from django.utils.translation import gettext as _

View File

@ -3,9 +3,6 @@ Common database model definitions.
These models are 'generic' and do not fit a particular business logic object.
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import decimal
import math

View File

@ -2,9 +2,6 @@
JSON serializers for common components
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.helpers import get_objectreference

View File

@ -2,9 +2,6 @@
User-configurable settings for the common app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from moneyed import CURRENCIES
from django.conf import settings

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,6 +1,3 @@
"""
Unit tests for the views associated with the 'common' app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

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

@ -2,9 +2,6 @@
Django views for interacting with common models
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
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 import admin
from import_export.admin import ImportExportModelAdmin

View File

@ -2,9 +2,6 @@
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 import rest_framework as rest_filters

View File

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

View File

@ -2,9 +2,6 @@
Django Forms for interacting with Company app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField

View File

@ -2,9 +2,6 @@
Company database model definitions
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from django.utils.translation import gettext_lazy as _

View File

@ -1,8 +1,5 @@
""" Unit tests for Company views (see views.py) """
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model

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

@ -2,10 +2,6 @@
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.views.generic import DetailView, ListView

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

@ -2,9 +2,6 @@
Label printing models
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import os
import logging

View File

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

View File

@ -1,8 +1,5 @@
# Tests for labels
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase

View File

@ -1,8 +1,5 @@
# Tests for labels
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
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 import_export.admin import ImportExportModelAdmin

View File

@ -2,9 +2,6 @@
JSON API for the Order app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.urls import include, path, re_path
from django.db.models import Q, F

View File

@ -2,9 +2,6 @@
Django Forms for interacting with Order objects
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import forms
from django.utils.translation import gettext_lazy as _

View File

@ -2,9 +2,6 @@
JSON serializers for the Order API
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
from django.utils.translation import gettext_lazy as _

View File

@ -1,8 +1,5 @@
""" Unit tests for Order views (see views.py) """
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model

View File

@ -2,9 +2,6 @@
Django views for interacting with Order app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db.utils import IntegrityError
from django.http.response import JsonResponse
from django.shortcuts import get_object_or_404

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

@ -2,9 +2,6 @@
Provides a JSON API for the Part app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
from django.urls import include, path, re_path

View File

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

View File

@ -2,9 +2,6 @@
Django Forms for interacting with Part objects
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import forms
from django.utils.translation import gettext_lazy as _

View File

@ -2,8 +2,6 @@
Part database model definitions
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import decimal
import os
@ -61,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
@ -2293,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

@ -2,9 +2,6 @@
User-configurable settings for the Part app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from common.models import InvenTreeSetting

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

@ -589,32 +589,15 @@
// Get a list of the selected BOM items
var rows = $("#bom-table").bootstrapTable('getSelections');
// TODO - In the future, display (in the dialog) which items are going to be deleted
showQuestionDialog(
'{% trans "Delete selected BOM items?" %}',
'{% trans "All selected BOM items will be deleted" %}',
{
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();
});
if (rows.length == 0) {
rows = $('#bom-table').bootstrapTable('getData');
}
deleteBomItems(rows, {
success: function() {
$('#bom-table').bootstrapTable('refresh');
}
);
});
});
$('#bom-upload').click(function() {

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 Part Parameters
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase, TransactionTestCase
import django.core.exceptions as django_exceptions

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

@ -2,9 +2,6 @@
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.exceptions import ValidationError
from django.db import transaction

View File

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

View File

@ -2,9 +2,6 @@
JSON API for the plugin app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.urls import include, re_path

View File

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

View File

@ -2,9 +2,6 @@
Functions for triggering and responding to server side events
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
from django.conf import settings
@ -40,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
@ -75,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

@ -2,8 +2,10 @@
Import helper for events
"""
from plugin.base.event.events import trigger_event
from plugin.base.event.events import process_event, register_event, trigger_event
__all__ = [
'process_event',
'register_event',
'trigger_event',
]

View File

@ -2,8 +2,6 @@
Plugin model definitions
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import warnings
from django.utils.translation import gettext_lazy as _

View File

@ -2,9 +2,6 @@
JSON serializers for plugin app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import subprocess

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

@ -2,9 +2,6 @@
Report template model definitions
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import sys
import logging

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

@ -2,9 +2,6 @@
JSON API for the Stock app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from collections import OrderedDict
from datetime import datetime, timedelta

View File

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

View File

@ -2,9 +2,6 @@
Django Forms for interacting with Stock app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from InvenTree.forms import HelperForm
from .models import StockItem, StockItemTracking

View File

@ -2,10 +2,6 @@
Stock database model definitions
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from jinja2 import Template
@ -2024,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')
@ -2035,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

@ -2,9 +2,6 @@
JSON serializers for Stock app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
from datetime import datetime, timedelta
from django.db import transaction

View File

@ -2,9 +2,6 @@
Unit testing for the Stock API
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import io
import tablib

View File

@ -2,9 +2,6 @@
Django views for interacting with Stock app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from django.views.generic import DetailView, ListView

View File

@ -16,6 +16,7 @@
/* exported
constructBomUploadTable,
deleteBomItems,
downloadBomTemplate,
exportBom,
newPartFromBomWizard,
@ -647,7 +648,88 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
reloadParentTable();
}
});
}
function deleteBomItems(items, options={}) {
/* Delete the selected BOM items from the database
*/
function renderItem(item, opts={}) {
var sub_part = item.sub_part_detail;
var thumb = thumbnailImage(sub_part.thumbnail || sub_part.image);
var html = `
<tr>
<td>${thumb} ${sub_part.full_name}</td>
<td>${item.reference}</td>
<td>${item.quantity}
</tr>
`;
return html;
}
var rows = '';
items.forEach(function(item) {
rows += renderItem(item);
});
var html = `
<div class='alert alert-block alert-danger'>
{% trans "All selected BOM items will be deleted" %}
</div>
<table class='table table-striped table-condensed'>
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Reference" %}</th>
<th>{% trans "Quantity" %}</th>
</tr>
${rows}
</table>
`;
constructFormBody({}, {
title: '{% trans "Delete selected BOM items?" %}',
fields: {},
preFormContent: html,
submitText: '{% trans "Delete" %}',
submitClass: 'danger',
confirm: true,
onSubmit: function(fields, opts) {
// Individually send DELETE requests for each BOM item
// We do *not* send these all at once, to prevent overloading the server
// Show the progress spinner
$(opts.modal).find('#modal-progress-spinner').show();
function deleteNextBomItem() {
if (items.length > 0) {
var item = items.shift();
inventreeDelete(`/api/bom/${item.pk}/`,
{
complete: deleteNextBomItem,
}
);
} else {
// Destroy this modal once all items are deleted
$(opts.modal).modal('hide');
if (options.success) {
options.success();
}
}
}
deleteNextBomItem();
},
});
}
@ -1146,16 +1228,10 @@ function loadBomTable(table, options={}) {
var pk = $(this).attr('pk');
var html = `
<div class='alert alert-block alert-danger'>
{% trans "Are you sure you want to delete this BOM item?" %}
</div>`;
var item = table.bootstrapTable('getRowByUniqueId', pk);
constructForm(`/api/bom/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete BOM Item" %}',
preFormContent: html,
onSuccess: function() {
deleteBomItems([item], {
success: function() {
reloadBomTable(table);
}
});

View File

@ -288,6 +288,7 @@ function constructDeleteForm(fields, options) {
* - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE' (default='PATCH')
* - title: The form title
* - submitText: Text for the "submit" button
* - submitClass: CSS class for the "submit" button (default = ')
* - closeText: Text for the "close" button
* - fields: list of fields to display, with the following options
* - filters: API query filters

View File

@ -42,6 +42,8 @@ function createNewModal(options={}) {
}
});
var submitClass = options.submitClass || 'primary';
var html = `
<div class='modal fade modal-fixed-footer modal-primary inventree-modal' role='dialog' id='modal-form-${id}' tabindex='-1'>
<div class='modal-dialog'>
@ -74,7 +76,7 @@ function createNewModal(options={}) {
<span class='flex-item' style='flex-grow: 1;'></span>
<h4><span id='modal-progress-spinner' class='fas fa-circle-notch fa-spin' style='display: none;'></span></h4>
<button type='button' class='btn btn-secondary' id='modal-form-close' data-bs-dismiss='modal'>{% trans "Cancel" %}</button>
<button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button>
<button type='button' class='btn btn-${submitClass}' id='modal-form-submit'>{% trans "Submit" %}</button>
</div>
</div>
</div>

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

@ -2,9 +2,6 @@
Test that the root API endpoint is available.
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import json
import requests

View File

@ -7,9 +7,6 @@ This is because the "translated" javascript files are compiled into the "static"
They should only contain template tags that render static information.
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import re
import os

View File

@ -1,8 +1,5 @@
""" Check that there are no database migration files which have not been committed. """
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import subprocess

Some files were not shown because too many files have changed in this diff Show More