mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into shipment-assign-fix
This commit is contained in:
commit
f4b470c396
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
import users.models
|
||||
|
@ -2,9 +2,6 @@
|
||||
Serializers used in various InvenTree apps
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import tablib
|
||||
|
||||
|
@ -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():
|
||||
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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'))
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
"""
|
||||
Django Forms for interacting with Build objects
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
@ -2,8 +2,6 @@
|
||||
Build database model definitions
|
||||
"""
|
||||
|
||||
# -*- 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):
|
||||
|
@ -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(
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
@ -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
|
||||
|
@ -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 _
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
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 plugin.models import NotificationUserSetting
|
||||
from part.test_part import BaseNotificationIntegrationTest
|
||||
|
@ -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,)
|
||||
|
@ -1,6 +1,3 @@
|
||||
"""
|
||||
Unit tests for the views associated with the 'common' app
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
@ -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 _
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 _
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||
|
@ -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,
|
||||
|
@ -2,9 +2,6 @@
|
||||
Label printing models
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from InvenTree.serializers import InvenTreeModelSerializer
|
||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Tests for labels
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 _
|
||||
|
||||
|
@ -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 _
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
@ -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 _
|
||||
|
||||
|
@ -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):
|
||||
|
@ -2,9 +2,6 @@
|
||||
User-configurable settings for the Part app
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
if (rows.length == 0) {
|
||||
rows = $('#bom-table').bootstrapTable('getData');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
deleteBomItems(rows, {
|
||||
success: function() {
|
||||
$('#bom-table').bootstrapTable('refresh');
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
$('#bom-upload').click(function() {
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import PIL
|
||||
|
||||
from django.urls import reverse
|
||||
|
@ -1,6 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from django.db import transaction
|
||||
|
||||
from django.test import TestCase
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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 _
|
||||
|
@ -2,9 +2,6 @@
|
||||
JSON serializers for plugin app
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
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.urls import include, path, re_path
|
||||
|
@ -2,9 +2,6 @@
|
||||
Report template model definitions
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from InvenTree.serializers import InvenTreeModelSerializer
|
||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -2,9 +2,6 @@
|
||||
Unit testing for the Stock API
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import io
|
||||
import tablib
|
||||
|
@ -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
|
||||
|
@ -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,19 +1228,13 @@ 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for "edit" button
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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.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.apps import AppConfig
|
||||
|
@ -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
|
||||
|
@ -2,9 +2,6 @@
|
||||
Test that the root API endpoint is available.
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user