mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into webp-support
This commit is contained in:
commit
dc2351748c
2
.github/workflows/qc_checks.yaml
vendored
2
.github/workflows/qc_checks.yaml
vendored
@ -12,7 +12,7 @@ on:
|
|||||||
- l10*
|
- l10*
|
||||||
|
|
||||||
env:
|
env:
|
||||||
python_version: 3.8
|
python_version: 3.9
|
||||||
node_version: 16
|
node_version: 16
|
||||||
|
|
||||||
server_start_sleep: 60
|
server_start_sleep: 60
|
||||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
stale-issue-message: 'This issue seems stale. Please react to show this is still important.'
|
stale-issue-message: 'This issue seems stale. Please react to show this is still important.'
|
||||||
stale-pr-message: 'This PR seems stale. Please react to show this is still important.'
|
stale-pr-message: 'This PR seems stale. Please react to show this is still important.'
|
||||||
stale-issue-label: 'no-activity'
|
stale-issue-label: 'inactive'
|
||||||
stale-pr-label: 'no-activity'
|
stale-pr-label: 'inactive'
|
||||||
start-date: '2022-01-01'
|
start-date: '2022-01-01'
|
||||||
exempt-all-milestones: true
|
exempt-all-milestones: true
|
||||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@ -21,10 +21,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.9
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
9
.github/workflows/welcome.yml
vendored
9
.github/workflows/welcome.yml
vendored
@ -16,5 +16,10 @@ jobs:
|
|||||||
- uses: actions/first-interaction@v1
|
- uses: actions/first-interaction@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
issue-message: 'Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.\nIf you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).'
|
issue-message: |
|
||||||
pr-message: 'This is your first PR, welcome!\nPlease check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.\nMake sure to document why this PR is needed and to link connected issues so we can review it faster.'
|
Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.
|
||||||
|
If you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).
|
||||||
|
pr-message: |
|
||||||
|
This is your first PR, welcome!
|
||||||
|
Please check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.
|
||||||
|
Make sure to document why this PR is needed and to link connected issues so we can review it faster.
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Main JSON interface views
|
Main JSON interface views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
@ -13,15 +10,11 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from .views import AjaxView
|
from .views import AjaxView
|
||||||
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
||||||
from .status import is_worker_running
|
from .status import is_worker_running
|
||||||
|
|
||||||
from plugin import registry
|
|
||||||
|
|
||||||
|
|
||||||
class InfoView(AjaxView):
|
class InfoView(AjaxView):
|
||||||
""" Simple JSON endpoint for InvenTree information.
|
""" Simple JSON endpoint for InvenTree information.
|
||||||
@ -119,40 +112,3 @@ class AttachmentMixin:
|
|||||||
attachment = serializer.save()
|
attachment = serializer.save()
|
||||||
attachment.user = self.request.user
|
attachment.user = self.request.user
|
||||||
attachment.save()
|
attachment.save()
|
||||||
|
|
||||||
|
|
||||||
class ActionPluginView(APIView):
|
|
||||||
"""
|
|
||||||
Endpoint for running custom action plugins.
|
|
||||||
"""
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
permissions.IsAuthenticated,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
|
|
||||||
action = request.data.get('action', None)
|
|
||||||
|
|
||||||
data = request.data.get('data', None)
|
|
||||||
|
|
||||||
if action is None:
|
|
||||||
return Response({
|
|
||||||
'error': _("No action specified")
|
|
||||||
})
|
|
||||||
|
|
||||||
action_plugins = registry.with_mixin('action')
|
|
||||||
for plugin in action_plugins:
|
|
||||||
if plugin.action_name() == action:
|
|
||||||
# TODO @matmair use easier syntax once InvenTree 0.7.0 is released
|
|
||||||
plugin.init(request.user, data=data)
|
|
||||||
|
|
||||||
plugin.perform_action()
|
|
||||||
|
|
||||||
return Response(plugin.get_response())
|
|
||||||
|
|
||||||
# If we got to here, no matching action was found
|
|
||||||
return Response({
|
|
||||||
'error': _("No matching action found"),
|
|
||||||
"action": action,
|
|
||||||
})
|
|
||||||
|
@ -142,6 +142,18 @@ class InvenTreeAPITestCase(APITestCase):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def put(self, url, data, expected_code=None, format='json'):
|
||||||
|
"""
|
||||||
|
Issue a PUT request
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.put(url, data=data, format=format)
|
||||||
|
|
||||||
|
if expected_code is not None:
|
||||||
|
self.assertEqual(response.status_code, expected_code)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def options(self, url, expected_code=None):
|
def options(self, url, expected_code=None):
|
||||||
"""
|
"""
|
||||||
Issue an OPTIONS request
|
Issue an OPTIONS request
|
||||||
|
@ -4,11 +4,16 @@ InvenTree API version information
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 48
|
INVENTREE_API_VERSION = 49
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v49 -> 2022-05-09 : https://github.com/inventree/InvenTree/pull/2957
|
||||||
|
- Allows filtering of plugin list by 'active' status
|
||||||
|
- Allows filtering of plugin list by 'mixin' support
|
||||||
|
- Adds endpoint to "identify" or "locate" stock items and locations (using plugins)
|
||||||
|
|
||||||
v48 -> 2022-05-12 : https://github.com/inventree/InvenTree/pull/2977
|
v48 -> 2022-05-12 : https://github.com/inventree/InvenTree/pull/2977
|
||||||
- Adds "export to file" functionality for PurchaseOrder API endpoint
|
- Adds "export to file" functionality for PurchaseOrder API endpoint
|
||||||
- Adds "export to file" functionality for SalesOrder API endpoint
|
- Adds "export to file" functionality for SalesOrder API endpoint
|
||||||
|
@ -131,7 +131,7 @@ class InvenTreeConfig(AppConfig):
|
|||||||
update = True
|
update = True
|
||||||
|
|
||||||
# Backend currency has changed?
|
# Backend currency has changed?
|
||||||
if not base_currency == backend.base_currency:
|
if base_currency != backend.base_currency:
|
||||||
logger.info(f"Base currency changed from {backend.base_currency} to {base_currency}")
|
logger.info(f"Base currency changed from {backend.base_currency} to {base_currency}")
|
||||||
update = True
|
update = True
|
||||||
|
|
||||||
@ -197,8 +197,6 @@ class InvenTreeConfig(AppConfig):
|
|||||||
logger.info(f'User {str(new_user)} was created!')
|
logger.info(f'User {str(new_user)} was created!')
|
||||||
except IntegrityError as _e:
|
except IntegrityError as _e:
|
||||||
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
|
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
|
||||||
if settings.TESTING_ENV:
|
|
||||||
raise _e
|
|
||||||
|
|
||||||
# do not try again
|
# do not try again
|
||||||
settings.USER_ADDED = True
|
settings.USER_ADDED = True
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
""" Custom fields used in InvenTree """
|
""" Custom fields used in InvenTree """
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .validators import allowable_url_schemes
|
from .validators import allowable_url_schemes
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
Helper forms which subclass Django forms to provide additional functionality
|
Helper forms which subclass Django forms to provide additional functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -224,7 +224,7 @@ def increment(n):
|
|||||||
groups = result.groups()
|
groups = result.groups()
|
||||||
|
|
||||||
# If we cannot match the regex, then simply return the provided value
|
# If we cannot match the regex, then simply return the provided value
|
||||||
if not len(groups) == 2:
|
if len(groups) != 2:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
prefix, number = groups
|
prefix, number = groups
|
||||||
@ -536,7 +536,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
|||||||
raise ValidationError([_("No serial numbers found")])
|
raise ValidationError([_("No serial numbers found")])
|
||||||
|
|
||||||
# The number of extracted serial numbers must match the expected quantity
|
# The number of extracted serial numbers must match the expected quantity
|
||||||
if not expected_quantity == len(numbers):
|
if expected_quantity != len(numbers):
|
||||||
raise ValidationError([_("Number of unique serial numbers ({s}) must match quantity ({q})").format(s=len(numbers), q=expected_quantity)])
|
raise ValidationError([_("Number of unique serial numbers ({s}) must match quantity ({q})").format(s=len(numbers), q=expected_quantity)])
|
||||||
|
|
||||||
return numbers
|
return numbers
|
||||||
@ -575,7 +575,7 @@ def validateFilterString(value, model=None):
|
|||||||
|
|
||||||
pair = group.split('=')
|
pair = group.split('=')
|
||||||
|
|
||||||
if not len(pair) == 2:
|
if len(pair) != 2:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"Invalid group: {g}".format(g=group)
|
"Invalid group: {g}".format(g=group)
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@ -250,7 +246,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
field_info = super().get_field_info(field)
|
field_info = super().get_field_info(field)
|
||||||
|
|
||||||
# If a default value is specified for the serializer field, add it!
|
# If a default value is specified for the serializer field, add it!
|
||||||
if 'default' not in field_info and not field.default == empty:
|
if 'default' not in field_info and field.default != empty:
|
||||||
field_info['default'] = field.get_default()
|
field_info['default'] = field.get_default()
|
||||||
|
|
||||||
# Force non-nullable fields to read as "required"
|
# Force non-nullable fields to read as "required"
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
Generic models which provide extra functionality over base Django model types.
|
Generic models which provide extra functionality over base Django model types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
@ -259,7 +257,7 @@ class InvenTreeAttachment(models.Model):
|
|||||||
new_file = os.path.abspath(new_file)
|
new_file = os.path.abspath(new_file)
|
||||||
|
|
||||||
# Check that there are no directory tricks going on...
|
# Check that there are no directory tricks going on...
|
||||||
if not os.path.dirname(new_file) == attachment_dir:
|
if os.path.dirname(new_file) != attachment_dir:
|
||||||
logger.error(f"Attempted to rename attachment outside valid directory: '{new_file}'")
|
logger.error(f"Attempted to rename attachment outside valid directory: '{new_file}'")
|
||||||
raise ValidationError(_("Invalid attachment directory"))
|
raise ValidationError(_("Invalid attachment directory"))
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
import users.models
|
import users.models
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Serializers used in various InvenTree apps
|
Serializers used in various InvenTree apps
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tablib
|
import tablib
|
||||||
|
|
||||||
|
@ -658,7 +658,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
|
|
||||||
EXTRA_URL_SCHEMES = CONFIG.get('extra_url_schemes', [])
|
EXTRA_URL_SCHEMES = CONFIG.get('extra_url_schemes', [])
|
||||||
|
|
||||||
if not type(EXTRA_URL_SCHEMES) in [list]: # pragma: no cover
|
if type(EXTRA_URL_SCHEMES) not in [list]: # pragma: no cover
|
||||||
logger.warning("extra_url_schemes not correctly formatted")
|
logger.warning("extra_url_schemes not correctly formatted")
|
||||||
EXTRA_URL_SCHEMES = []
|
EXTRA_URL_SCHEMES = []
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import warnings
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -11,6 +9,8 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
from django.core import mail as django_mail
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
@ -52,6 +52,15 @@ def schedule_task(taskname, **kwargs):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def raise_warning(msg):
|
||||||
|
"""Log and raise a warning"""
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
|
# If testing is running raise a warning that can be asserted
|
||||||
|
if settings.TESTING:
|
||||||
|
warnings.warn(msg)
|
||||||
|
|
||||||
|
|
||||||
def offload_task(taskname, *args, force_sync=False, **kwargs):
|
def offload_task(taskname, *args, force_sync=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create an AsyncTask if workers are running.
|
Create an AsyncTask if workers are running.
|
||||||
@ -67,28 +76,38 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
from InvenTree.status import is_worker_running
|
from InvenTree.status import is_worker_running
|
||||||
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
|
logger.warning(f"Could not offload task '{taskname}' - app registry not ready")
|
||||||
|
return
|
||||||
|
except (OperationalError, ProgrammingError): # pragma: no cover
|
||||||
|
raise_warning(f"Could not offload task '{taskname}' - database not ready")
|
||||||
|
|
||||||
if is_worker_running() and not force_sync: # pragma: no cover
|
if is_worker_running() and not force_sync: # pragma: no cover
|
||||||
# Running as asynchronous task
|
# Running as asynchronous task
|
||||||
try:
|
try:
|
||||||
task = AsyncTask(taskname, *args, **kwargs)
|
task = AsyncTask(taskname, *args, **kwargs)
|
||||||
task.run()
|
task.run()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
|
raise_warning(f"WARNING: '{taskname}' not started - Function not found")
|
||||||
|
else:
|
||||||
|
|
||||||
|
if callable(taskname):
|
||||||
|
# function was passed - use that
|
||||||
|
_func = taskname
|
||||||
else:
|
else:
|
||||||
# Split path
|
# Split path
|
||||||
try:
|
try:
|
||||||
app, mod, func = taskname.split('.')
|
app, mod, func = taskname.split('.')
|
||||||
app_mod = app + '.' + mod
|
app_mod = app + '.' + mod
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
raise_warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Import module from app
|
# Import module from app
|
||||||
try:
|
try:
|
||||||
_mod = importlib.import_module(app_mod)
|
_mod = importlib.import_module(app_mod)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
raise_warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Retrieve function
|
# Retrieve function
|
||||||
@ -102,17 +121,11 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
if not _func:
|
if not _func:
|
||||||
_func = eval(func) # pragma: no cover
|
_func = eval(func) # pragma: no cover
|
||||||
except NameError:
|
except NameError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
raise_warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Workers are not running: run it as synchronous task
|
# Workers are not running: run it as synchronous task
|
||||||
_func(*args, **kwargs)
|
_func(*args, **kwargs)
|
||||||
|
|
||||||
except AppRegistryNotReady: # pragma: no cover
|
|
||||||
logger.warning(f"Could not offload task '{taskname}' - app registry not ready")
|
|
||||||
return
|
|
||||||
except (OperationalError, ProgrammingError): # pragma: no cover
|
|
||||||
logger.warning(f"Could not offload task '{taskname}' - database not ready")
|
|
||||||
|
|
||||||
|
|
||||||
def heartbeat():
|
def heartbeat():
|
||||||
@ -126,8 +139,8 @@ def heartbeat():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from django_q.models import Success
|
from django_q.models import Success
|
||||||
logger.info("Could not perform heartbeat task - App registry not ready")
|
|
||||||
except AppRegistryNotReady: # pragma: no cover
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
|
logger.info("Could not perform heartbeat task - App registry not ready")
|
||||||
return
|
return
|
||||||
|
|
||||||
threshold = timezone.now() - timedelta(minutes=30)
|
threshold = timezone.now() - timedelta(minutes=30)
|
||||||
@ -204,26 +217,26 @@ def check_for_updates():
|
|||||||
|
|
||||||
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
||||||
|
|
||||||
if not response.status_code == 200:
|
if response.status_code != 200:
|
||||||
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
|
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}') # pragma: no cover
|
||||||
|
|
||||||
data = json.loads(response.text)
|
data = json.loads(response.text)
|
||||||
|
|
||||||
tag = data.get('tag_name', None)
|
tag = data.get('tag_name', None)
|
||||||
|
|
||||||
if not tag:
|
if not tag:
|
||||||
raise ValueError("'tag_name' missing from GitHub response")
|
raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover
|
||||||
|
|
||||||
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
||||||
|
|
||||||
if not len(match.groups()) == 3:
|
if len(match.groups()) != 3: # pragma: no cover
|
||||||
logger.warning(f"Version '{tag}' did not match expected pattern")
|
logger.warning(f"Version '{tag}' did not match expected pattern")
|
||||||
return
|
return
|
||||||
|
|
||||||
latest_version = [int(x) for x in match.groups()]
|
latest_version = [int(x) for x in match.groups()]
|
||||||
|
|
||||||
if not len(latest_version) == 3:
|
if len(latest_version) != 3:
|
||||||
raise ValueError(f"Version '{tag}' is not correct format")
|
raise ValueError(f"Version '{tag}' is not correct format") # pragma: no cover
|
||||||
|
|
||||||
logger.info(f"Latest InvenTree version: '{tag}'")
|
logger.info(f"Latest InvenTree version: '{tag}'")
|
||||||
|
|
||||||
@ -288,7 +301,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
|
|||||||
recipients = [recipients]
|
recipients = [recipients]
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
'django.core.mail.send_mail',
|
django_mail.send_mail,
|
||||||
subject,
|
subject,
|
||||||
body,
|
body,
|
||||||
from_email,
|
from_email,
|
||||||
|
@ -96,6 +96,12 @@ class HTMLAPITests(TestCase):
|
|||||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_not_found(self):
|
||||||
|
"""Test that the NotFoundView is working"""
|
||||||
|
|
||||||
|
response = self.client.get('/api/anc')
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
class APITests(InvenTreeAPITestCase):
|
class APITests(InvenTreeAPITestCase):
|
||||||
""" Tests for the InvenTree API """
|
""" Tests for the InvenTree API """
|
||||||
|
@ -2,10 +2,20 @@
|
|||||||
Unit tests for task management
|
Unit tests for task management
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django_q.models import Schedule
|
from django_q.models import Schedule
|
||||||
|
|
||||||
|
from error_report.models import Error
|
||||||
|
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
|
||||||
|
threshold = timezone.now() - timedelta(days=30)
|
||||||
|
threshold_low = threshold - timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
class ScheduledTaskTests(TestCase):
|
class ScheduledTaskTests(TestCase):
|
||||||
@ -41,3 +51,79 @@ class ScheduledTaskTests(TestCase):
|
|||||||
# But the 'minutes' should have been updated
|
# But the 'minutes' should have been updated
|
||||||
t = Schedule.objects.get(func=task)
|
t = Schedule.objects.get(func=task)
|
||||||
self.assertEqual(t.minutes, 5)
|
self.assertEqual(t.minutes, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def get_result():
|
||||||
|
"""Demo function for test_offloading"""
|
||||||
|
return 'abc'
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeTaskTests(TestCase):
|
||||||
|
"""Unit tests for tasks"""
|
||||||
|
|
||||||
|
def test_offloading(self):
|
||||||
|
"""Test task offloading"""
|
||||||
|
|
||||||
|
# Run with function ref
|
||||||
|
InvenTree.tasks.offload_task(get_result)
|
||||||
|
|
||||||
|
# Run with string ref
|
||||||
|
InvenTree.tasks.offload_task('InvenTree.test_tasks.get_result')
|
||||||
|
|
||||||
|
# Error runs
|
||||||
|
# Malformed taskname
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTree')
|
||||||
|
|
||||||
|
# Non exsistent app
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter')
|
||||||
|
|
||||||
|
# Non exsistent function
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist')
|
||||||
|
|
||||||
|
def test_task_hearbeat(self):
|
||||||
|
"""Test the task heartbeat"""
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat)
|
||||||
|
|
||||||
|
def test_task_delete_successful_tasks(self):
|
||||||
|
"""Test the task delete_successful_tasks"""
|
||||||
|
from django_q.models import Success
|
||||||
|
|
||||||
|
Success.objects.create(name='abc', func='abc', stopped=threshold, started=threshold_low)
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks)
|
||||||
|
results = Success.objects.filter(started__lte=threshold)
|
||||||
|
self.assertEqual(len(results), 0)
|
||||||
|
|
||||||
|
def test_task_delete_old_error_logs(self):
|
||||||
|
"""Test the task delete_old_error_logs"""
|
||||||
|
|
||||||
|
# Create error
|
||||||
|
error_obj = Error.objects.create()
|
||||||
|
error_obj.when = threshold_low
|
||||||
|
error_obj.save()
|
||||||
|
|
||||||
|
# Check that it is not empty
|
||||||
|
errors = Error.objects.filter(when__lte=threshold,)
|
||||||
|
self.assertNotEqual(len(errors), 0)
|
||||||
|
|
||||||
|
# Run action
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_error_logs)
|
||||||
|
|
||||||
|
# Check that it is empty again
|
||||||
|
errors = Error.objects.filter(when__lte=threshold,)
|
||||||
|
self.assertEqual(len(errors), 0)
|
||||||
|
|
||||||
|
def test_task_check_for_updates(self):
|
||||||
|
"""Test the task check_for_updates"""
|
||||||
|
# Check that setting should be empty
|
||||||
|
self.assertEqual(InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION'), '')
|
||||||
|
|
||||||
|
# Get new version
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates)
|
||||||
|
|
||||||
|
# Check that setting is not empty
|
||||||
|
response = InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION')
|
||||||
|
self.assertNotEqual(response, '')
|
||||||
|
self.assertTrue(bool(response))
|
||||||
|
@ -451,6 +451,11 @@ class TestSettings(TestCase):
|
|||||||
self.user_mdl = get_user_model()
|
self.user_mdl = get_user_model()
|
||||||
self.env = support.EnvironmentVarGuard()
|
self.env = support.EnvironmentVarGuard()
|
||||||
|
|
||||||
|
# Create a user for auth
|
||||||
|
user = get_user_model()
|
||||||
|
self.user = user.objects.create_superuser('testuser1', 'test1@testing.com', 'password1')
|
||||||
|
self.client.login(username='testuser1', password='password1')
|
||||||
|
|
||||||
def run_reload(self):
|
def run_reload(self):
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
@ -467,23 +472,49 @@ class TestSettings(TestCase):
|
|||||||
|
|
||||||
# nothing set
|
# nothing set
|
||||||
self.run_reload()
|
self.run_reload()
|
||||||
self.assertEqual(user_count(), 0)
|
self.assertEqual(user_count(), 1)
|
||||||
|
|
||||||
# not enough set
|
# not enough set
|
||||||
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
||||||
self.run_reload()
|
self.run_reload()
|
||||||
self.assertEqual(user_count(), 0)
|
self.assertEqual(user_count(), 1)
|
||||||
|
|
||||||
# enough set
|
# enough set
|
||||||
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
||||||
self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email
|
self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email
|
||||||
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password
|
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password
|
||||||
self.run_reload()
|
self.run_reload()
|
||||||
self.assertEqual(user_count(), 1)
|
self.assertEqual(user_count(), 2)
|
||||||
|
|
||||||
|
# create user manually
|
||||||
|
self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||||
|
self.assertEqual(user_count(), 3)
|
||||||
|
# check it will not be created again
|
||||||
|
self.env.set('INVENTREE_ADMIN_USER', 'testuser')
|
||||||
|
self.env.set('INVENTREE_ADMIN_EMAIL', 'test@testing.com')
|
||||||
|
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password')
|
||||||
|
self.run_reload()
|
||||||
|
self.assertEqual(user_count(), 3)
|
||||||
|
|
||||||
# make sure to clean up
|
# make sure to clean up
|
||||||
settings.TESTING_ENV = False
|
settings.TESTING_ENV = False
|
||||||
|
|
||||||
|
def test_initial_install(self):
|
||||||
|
"""Test if install of plugins on startup works"""
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
|
# Check an install run
|
||||||
|
response = registry.install_plugin_file()
|
||||||
|
self.assertEqual(response, 'first_run')
|
||||||
|
|
||||||
|
# Set dynamic setting to True and rerun to launch install
|
||||||
|
InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
|
||||||
|
registry.reload_plugins()
|
||||||
|
|
||||||
|
# Check that there was anotehr run
|
||||||
|
response = registry.install_plugin_file()
|
||||||
|
self.assertEqual(response, True)
|
||||||
|
|
||||||
def test_helpers_cfg_file(self):
|
def test_helpers_cfg_file(self):
|
||||||
# normal run - not configured
|
# normal run - not configured
|
||||||
self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file())
|
self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file())
|
||||||
|
@ -27,7 +27,7 @@ from order.api import order_api_urls
|
|||||||
from label.api import label_api_urls
|
from label.api import label_api_urls
|
||||||
from report.api import report_api_urls
|
from report.api import report_api_urls
|
||||||
from plugin.api import plugin_api_urls
|
from plugin.api import plugin_api_urls
|
||||||
from plugin.barcode import barcode_api_urls
|
from users.api import user_urls
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
@ -45,20 +45,11 @@ from .views import DynamicJsView
|
|||||||
from .views import NotificationsView
|
from .views import NotificationsView
|
||||||
|
|
||||||
from .api import InfoView, NotFoundView
|
from .api import InfoView, NotFoundView
|
||||||
from .api import ActionPluginView
|
|
||||||
|
|
||||||
from users.api import user_urls
|
|
||||||
|
|
||||||
admin.site.site_header = "InvenTree Admin"
|
admin.site.site_header = "InvenTree Admin"
|
||||||
|
|
||||||
apipatterns = []
|
|
||||||
|
|
||||||
if settings.PLUGINS_ENABLED:
|
apipatterns = [
|
||||||
apipatterns.append(
|
|
||||||
re_path(r'^plugin/', include(plugin_api_urls))
|
|
||||||
)
|
|
||||||
|
|
||||||
apipatterns += [
|
|
||||||
re_path(r'^settings/', include(settings_api_urls)),
|
re_path(r'^settings/', include(settings_api_urls)),
|
||||||
re_path(r'^part/', include(part_api_urls)),
|
re_path(r'^part/', include(part_api_urls)),
|
||||||
re_path(r'^bom/', include(bom_api_urls)),
|
re_path(r'^bom/', include(bom_api_urls)),
|
||||||
@ -68,13 +59,10 @@ apipatterns += [
|
|||||||
re_path(r'^order/', include(order_api_urls)),
|
re_path(r'^order/', include(order_api_urls)),
|
||||||
re_path(r'^label/', include(label_api_urls)),
|
re_path(r'^label/', include(label_api_urls)),
|
||||||
re_path(r'^report/', include(report_api_urls)),
|
re_path(r'^report/', include(report_api_urls)),
|
||||||
|
|
||||||
# User URLs
|
|
||||||
re_path(r'^user/', include(user_urls)),
|
re_path(r'^user/', include(user_urls)),
|
||||||
|
|
||||||
# Plugin endpoints
|
# Plugin endpoints
|
||||||
re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
|
path('', include(plugin_api_urls)),
|
||||||
re_path(r'^barcode/', include(barcode_api_urls)),
|
|
||||||
|
|
||||||
# Webhook enpoint
|
# Webhook enpoint
|
||||||
path('', include(common_api_urls)),
|
path('', include(common_api_urls)),
|
||||||
|
@ -5,8 +5,6 @@ In particular these views provide base functionality for rendering Django forms
|
|||||||
as JSON objects and passing them to modal forms (using jQuery / bootstrap).
|
as JSON objects and passing them to modal forms (using jQuery / bootstrap).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -627,7 +625,7 @@ class SetPasswordView(AjaxUpdateView):
|
|||||||
if valid:
|
if valid:
|
||||||
# Passwords must match
|
# Passwords must match
|
||||||
|
|
||||||
if not p1 == p2:
|
if p1 != p2:
|
||||||
error = _('Password fields must match')
|
error = _('Password fields must match')
|
||||||
form.add_error('enter_password', error)
|
form.add_error('enter_password', error)
|
||||||
form.add_error('confirm_password', error)
|
form.add_error('confirm_password', error)
|
||||||
@ -797,13 +795,9 @@ class CurrencyRefreshView(RedirectView):
|
|||||||
On a POST request we will attempt to refresh the exchange rates
|
On a POST request we will attempt to refresh the exchange rates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task, update_exchange_rates
|
||||||
|
|
||||||
# Define associated task from InvenTree.tasks list of methods
|
offload_task(update_exchange_rates, force_sync=True)
|
||||||
taskname = 'InvenTree.tasks.update_exchange_rates'
|
|
||||||
|
|
||||||
# Run it
|
|
||||||
offload_task(taskname, force_sync=True)
|
|
||||||
|
|
||||||
return redirect(reverse_lazy('settings'))
|
return redirect(reverse_lazy('settings'))
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
JSON API for the Build app
|
JSON API for the Build app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.urls import include, re_path
|
from django.urls import include, re_path
|
||||||
|
|
||||||
from rest_framework import filters, generics
|
from rest_framework import filters, generics
|
||||||
@ -285,6 +282,13 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
|
|||||||
API endpoint for deleting multiple build outputs
|
API endpoint for deleting multiple build outputs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
|
ctx['to_complete'] = False
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
queryset = Build.objects.none()
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
serializer_class = build.serializers.BuildOutputDeleteSerializer
|
serializer_class = build.serializers.BuildOutputDeleteSerializer
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
"""
|
|
||||||
Django Forms for interacting with Build objects
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
@ -2,8 +2,6 @@
|
|||||||
Build database model definitions
|
Build database model definitions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -777,7 +775,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
if not output.is_building:
|
if not output.is_building:
|
||||||
raise ValidationError(_("Build output is already completed"))
|
raise ValidationError(_("Build output is already completed"))
|
||||||
|
|
||||||
if not output.build == self:
|
if output.build != self:
|
||||||
raise ValidationError(_("Build output does not match Build Order"))
|
raise ValidationError(_("Build output does not match Build Order"))
|
||||||
|
|
||||||
# Unallocate all build items against the output
|
# Unallocate all build items against the output
|
||||||
@ -1141,12 +1139,13 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Callback function to be executed after a Build instance is saved
|
Callback function to be executed after a Build instance is saved
|
||||||
"""
|
"""
|
||||||
|
from . import tasks as build_tasks
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
# A new Build has just been created
|
# A new Build has just been created
|
||||||
|
|
||||||
# Run checks on required parts
|
# Run checks on required parts
|
||||||
InvenTree.tasks.offload_task('build.tasks.check_build_stock', instance)
|
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
||||||
|
|
||||||
|
|
||||||
class BuildOrderAttachment(InvenTreeAttachment):
|
class BuildOrderAttachment(InvenTreeAttachment):
|
||||||
@ -1240,7 +1239,7 @@ class BuildItem(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Quantity must be 1 for serialized stock
|
# Quantity must be 1 for serialized stock
|
||||||
if self.stock_item.serialized and not self.quantity == 1:
|
if self.stock_item.serialized and self.quantity != 1:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'quantity': _('Quantity must be 1 for serialized stock')
|
'quantity': _('Quantity must be 1 for serialized stock')
|
||||||
})
|
})
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
JSON serializers for Build API
|
JSON serializers for Build API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -202,7 +199,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_quantity(self, quantity):
|
def validate_quantity(self, quantity):
|
||||||
|
|
||||||
if quantity < 0:
|
if quantity <= 0:
|
||||||
raise ValidationError(_("Quantity must be greater than zero"))
|
raise ValidationError(_("Quantity must be greater than zero"))
|
||||||
|
|
||||||
part = self.get_part()
|
part = self.get_part()
|
||||||
@ -212,7 +209,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
if part.trackable:
|
if part.trackable:
|
||||||
raise ValidationError(_("Integer quantity required for trackable parts"))
|
raise ValidationError(_("Integer quantity required for trackable parts"))
|
||||||
|
|
||||||
if part.has_trackable_parts():
|
if part.has_trackable_parts:
|
||||||
raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts"))
|
raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts"))
|
||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
@ -235,7 +232,6 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
serial_numbers = serial_numbers.strip()
|
serial_numbers = serial_numbers.strip()
|
||||||
|
|
||||||
# TODO: Field level validation necessary here?
|
|
||||||
return serial_numbers
|
return serial_numbers
|
||||||
|
|
||||||
auto_allocate = serializers.BooleanField(
|
auto_allocate = serializers.BooleanField(
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -305,6 +302,215 @@ class BuildTest(BuildAPITest):
|
|||||||
|
|
||||||
self.assertEqual(bo.status, BuildStatus.CANCELLED)
|
self.assertEqual(bo.status, BuildStatus.CANCELLED)
|
||||||
|
|
||||||
|
def test_create_delete_output(self):
|
||||||
|
"""
|
||||||
|
Test that we can create and delete build outputs via the API
|
||||||
|
"""
|
||||||
|
|
||||||
|
bo = Build.objects.get(pk=1)
|
||||||
|
|
||||||
|
n_outputs = bo.output_count
|
||||||
|
|
||||||
|
create_url = reverse('api-build-output-create', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
# Attempt to create outputs with invalid data
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 'not a number',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('A valid number is required', str(response.data))
|
||||||
|
|
||||||
|
for q in [-100, -10.3, 0]:
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': q,
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
if q == 0:
|
||||||
|
self.assertIn('Quantity must be greater than zero', str(response.data))
|
||||||
|
else:
|
||||||
|
self.assertIn('Ensure this value is greater than or equal to 0', str(response.data))
|
||||||
|
|
||||||
|
# Mark the part being built as 'trackable' (requires integer quantity)
|
||||||
|
bo.part.trackable = True
|
||||||
|
bo.part.save()
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 12.3,
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('Integer quantity required for trackable parts', str(response.data))
|
||||||
|
|
||||||
|
# Erroneous serial numbers
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 5,
|
||||||
|
'serial_numbers': '1, 2, 3, 4, 5, 6',
|
||||||
|
'batch': 'my-batch',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('Number of unique serial numbers (6) must match quantity (5)', str(response.data))
|
||||||
|
|
||||||
|
# At this point, no new build outputs should have been created
|
||||||
|
self.assertEqual(n_outputs, bo.output_count)
|
||||||
|
|
||||||
|
# Now, create with *good* data
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 5,
|
||||||
|
'serial_numbers': '1, 2, 3, 4, 5',
|
||||||
|
'batch': 'my-batch',
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5 new outputs have been created
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
|
||||||
|
# Attempt to create with identical serial numbers
|
||||||
|
response = self.post(
|
||||||
|
create_url,
|
||||||
|
{
|
||||||
|
'quantity': 3,
|
||||||
|
'serial_numbers': '1-3',
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('The following serial numbers already exist : 1,2,3', str(response.data))
|
||||||
|
|
||||||
|
# Double check no new outputs have been created
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
|
||||||
|
# Now, let's delete each build output individually via the API
|
||||||
|
outputs = bo.build_outputs.all()
|
||||||
|
|
||||||
|
delete_url = reverse('api-build-output-delete', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
delete_url,
|
||||||
|
{
|
||||||
|
'outputs': [],
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('A list of build outputs must be provided', str(response.data))
|
||||||
|
|
||||||
|
# Mark 1 build output as complete
|
||||||
|
bo.complete_build_output(outputs[0], self.user)
|
||||||
|
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
self.assertEqual(1, bo.complete_count)
|
||||||
|
|
||||||
|
# Delete all outputs at once
|
||||||
|
# Note: One has been completed, so this should fail!
|
||||||
|
response = self.post(
|
||||||
|
delete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': output.pk,
|
||||||
|
} for output in outputs
|
||||||
|
]
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('This build output has already been completed', str(response.data))
|
||||||
|
|
||||||
|
# No change to the build outputs
|
||||||
|
self.assertEqual(n_outputs + 5, bo.output_count)
|
||||||
|
self.assertEqual(1, bo.complete_count)
|
||||||
|
|
||||||
|
# Let's delete 2 build outputs
|
||||||
|
response = self.post(
|
||||||
|
delete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': output.pk,
|
||||||
|
} for output in outputs[1:3]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
expected_code=201
|
||||||
|
)
|
||||||
|
|
||||||
|
# Two build outputs have been removed
|
||||||
|
self.assertEqual(n_outputs + 3, bo.output_count)
|
||||||
|
self.assertEqual(1, bo.complete_count)
|
||||||
|
|
||||||
|
# Tests for BuildOutputComplete serializer
|
||||||
|
complete_url = reverse('api-build-output-complete', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
# Let's mark the remaining outputs as complete
|
||||||
|
response = self.post(
|
||||||
|
complete_url,
|
||||||
|
{
|
||||||
|
'outputs': [],
|
||||||
|
'location': 4,
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('A list of build outputs must be provided', str(response.data))
|
||||||
|
|
||||||
|
for output in outputs[3:]:
|
||||||
|
output.refresh_from_db()
|
||||||
|
self.assertTrue(output.is_building)
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
complete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': output.pk
|
||||||
|
} for output in outputs[3:]
|
||||||
|
],
|
||||||
|
'location': 4,
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the outputs have been completed
|
||||||
|
self.assertEqual(3, bo.complete_count)
|
||||||
|
|
||||||
|
for output in outputs[3:]:
|
||||||
|
output.refresh_from_db()
|
||||||
|
self.assertEqual(output.location.pk, 4)
|
||||||
|
self.assertFalse(output.is_building)
|
||||||
|
|
||||||
|
# Try again, with an output which has already been completed
|
||||||
|
response = self.post(
|
||||||
|
complete_url,
|
||||||
|
{
|
||||||
|
'outputs': [
|
||||||
|
{
|
||||||
|
'output': outputs.last().pk,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('This build output has already been completed', str(response.data))
|
||||||
|
|
||||||
|
|
||||||
class BuildAllocationTest(BuildAPITest):
|
class BuildAllocationTest(BuildAPITest):
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Django views for interacting with Build objects
|
Django views for interacting with Build objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Provides a JSON API for common components.
|
Provides a JSON API for common components.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
|
@ -199,7 +199,7 @@ class FileManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Excel import casts number-looking-items into floats, which is annoying
|
# Excel import casts number-looking-items into floats, which is annoying
|
||||||
if item == int(item) and not str(item) == str(int(item)):
|
if item == int(item) and str(item) != str(int(item)):
|
||||||
data[idx] = int(item)
|
data[idx] = int(item)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Django forms for interacting with common objects
|
Django forms for interacting with common objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
@ -3,9 +3,6 @@ Common database model definitions.
|
|||||||
These models are 'generic' and do not fit a particular business logic object.
|
These models are 'generic' and do not fit a particular business logic object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import decimal
|
import decimal
|
||||||
import math
|
import math
|
||||||
@ -271,9 +268,9 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
plugin = kwargs.get('plugin', None)
|
plugin = kwargs.get('plugin', None)
|
||||||
|
|
||||||
if plugin is not None:
|
if plugin is not None:
|
||||||
from plugin import InvenTreePluginBase
|
from plugin import InvenTreePlugin
|
||||||
|
|
||||||
if issubclass(plugin.__class__, InvenTreePluginBase):
|
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||||
plugin = plugin.plugin_config()
|
plugin = plugin.plugin_config()
|
||||||
|
|
||||||
filters['plugin'] = plugin
|
filters['plugin'] = plugin
|
||||||
@ -375,9 +372,9 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
filters['user'] = user
|
filters['user'] = user
|
||||||
|
|
||||||
if plugin is not None:
|
if plugin is not None:
|
||||||
from plugin import InvenTreePluginBase
|
from plugin import InvenTreePlugin
|
||||||
|
|
||||||
if issubclass(plugin.__class__, InvenTreePluginBase):
|
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||||
filters['plugin'] = plugin.plugin_config()
|
filters['plugin'] = plugin.plugin_config()
|
||||||
else:
|
else:
|
||||||
filters['plugin'] = plugin
|
filters['plugin'] = plugin
|
||||||
@ -1802,10 +1799,8 @@ class WebhookEndpoint(models.Model):
|
|||||||
def process_webhook(self):
|
def process_webhook(self):
|
||||||
if self.token:
|
if self.token:
|
||||||
self.verify = VerificationMethod.TOKEN
|
self.verify = VerificationMethod.TOKEN
|
||||||
# TODO make a object-setting
|
|
||||||
if self.secret:
|
if self.secret:
|
||||||
self.verify = VerificationMethod.HMAC
|
self.verify = VerificationMethod.HMAC
|
||||||
# TODO make a object-setting
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_token(self, payload, headers, request):
|
def validate_token(self, payload, headers, request):
|
||||||
|
@ -108,7 +108,7 @@ class NotificationMethod:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Check if method globally enabled
|
# Check if method globally enabled
|
||||||
plg_instance = registry.plugins.get(plg_cls.PLUGIN_NAME.lower())
|
plg_instance = registry.plugins.get(plg_cls.NAME.lower())
|
||||||
if plg_instance and not plg_instance.get_setting(self.GLOBAL_SETTING):
|
if plg_instance and not plg_instance.get_setting(self.GLOBAL_SETTING):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
JSON serializers for common components
|
JSON serializers for common components
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
from InvenTree.helpers import get_objectreference
|
from InvenTree.helpers import get_objectreference
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
User-configurable settings for the common app
|
User-configurable settings for the common app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod, storage
|
from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod, storage
|
||||||
from plugin.models import NotificationUserSetting
|
from plugin.models import NotificationUserSetting
|
||||||
from part.test_part import BaseNotificationIntegrationTest
|
from part.test_part import BaseNotificationIntegrationTest
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from common.models import NotificationEntry
|
from common.models import NotificationEntry
|
||||||
|
from . import tasks as common_tasks
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
|
|
||||||
@ -14,4 +15,4 @@ class TaskTest(TestCase):
|
|||||||
|
|
||||||
# check empty run
|
# check empty run
|
||||||
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
||||||
offload_task('common.tasks.delete_old_notifications',)
|
offload_task(common_tasks.delete_old_notifications,)
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
"""
|
"""
|
||||||
Unit tests for the views associated with the 'common' app
|
Unit tests for the views associated with the 'common' app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import json
|
import json
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -10,7 +9,8 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
from plugin.models import NotificationUserSetting
|
from plugin.models import NotificationUserSetting, PluginConfig
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
|
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
|
||||||
from .api import WebhookView
|
from .api import WebhookView
|
||||||
@ -132,7 +132,7 @@ class SettingsTest(TestCase):
|
|||||||
if description is None:
|
if description is None:
|
||||||
raise ValueError(f'Missing GLOBAL_SETTING description for {key}') # pragma: no cover
|
raise ValueError(f'Missing GLOBAL_SETTING description for {key}') # pragma: no cover
|
||||||
|
|
||||||
if not key == key.upper():
|
if key != key.upper():
|
||||||
raise ValueError(f"SETTINGS key '{key}' is not uppercase") # pragma: no cover
|
raise ValueError(f"SETTINGS key '{key}' is not uppercase") # pragma: no cover
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
@ -477,15 +477,36 @@ class PluginSettingsApiTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.get(url, expected_code=200)
|
self.get(url, expected_code=200)
|
||||||
|
|
||||||
def test_invalid_plugin_slug(self):
|
def test_valid_plugin_slug(self):
|
||||||
"""Test that an invalid plugin slug returns a 404"""
|
"""Test that an valid plugin slug runs through"""
|
||||||
|
# load plugin configs
|
||||||
|
fixtures = PluginConfig.objects.all()
|
||||||
|
if not fixtures:
|
||||||
|
registry.reload_plugins()
|
||||||
|
fixtures = PluginConfig.objects.all()
|
||||||
|
|
||||||
|
# get data
|
||||||
|
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'API_KEY'})
|
||||||
|
response = self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
# check the right setting came through
|
||||||
|
self.assertTrue(response.data['key'], 'API_KEY')
|
||||||
|
self.assertTrue(response.data['plugin'], 'sample')
|
||||||
|
self.assertTrue(response.data['type'], 'string')
|
||||||
|
self.assertTrue(response.data['description'], 'Key required for accessing external API')
|
||||||
|
|
||||||
|
# Failure mode tests
|
||||||
|
|
||||||
|
# Non - exsistant plugin
|
||||||
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'})
|
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'})
|
||||||
|
|
||||||
response = self.get(url, expected_code=404)
|
response = self.get(url, expected_code=404)
|
||||||
|
|
||||||
self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
|
self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
|
||||||
|
|
||||||
|
# Wrong key
|
||||||
|
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'doesnotexsist'})
|
||||||
|
response = self.get(url, expected_code=404)
|
||||||
|
self.assertIn("Plugin 'sample' has no setting matching 'doesnotexsist'", str(response.data))
|
||||||
|
|
||||||
def test_invalid_setting_key(self):
|
def test_invalid_setting_key(self):
|
||||||
"""Test that an invalid setting key returns a 404"""
|
"""Test that an invalid setting key returns a 404"""
|
||||||
...
|
...
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Django views for interacting with common models
|
Django views for interacting with common models
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Provides a JSON API for the Company app
|
Provides a JSON API for the Company app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Django Forms for interacting with Company app
|
Django Forms for interacting with Company app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Company database model definitions
|
Company database model definitions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -494,7 +491,7 @@ class SupplierPart(models.Model):
|
|||||||
# Ensure that the linked manufacturer_part points to the same part!
|
# Ensure that the linked manufacturer_part points to the same part!
|
||||||
if self.manufacturer_part and self.part:
|
if self.manufacturer_part and self.part:
|
||||||
|
|
||||||
if not self.manufacturer_part.part == self.part:
|
if self.manufacturer_part.part != self.part:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'manufacturer_part': _("Linked manufacturer part must reference the same base part"),
|
'manufacturer_part': _("Linked manufacturer part must reference the same base part"),
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
""" Unit tests for Company views (see views.py) """
|
""" Unit tests for Company views (see views.py) """
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
@ -2,10 +2,6 @@
|
|||||||
Django views for interacting with Company app
|
Django views for interacting with Company app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
@ -162,7 +158,7 @@ class CompanyImageDownloadFromURL(AjaxUpdateView):
|
|||||||
self.response = response
|
self.response = response
|
||||||
|
|
||||||
# Check for valid response code
|
# Check for valid response code
|
||||||
if not response.status_code == 200:
|
if response.status_code != 200:
|
||||||
form.add_error('url', _('Invalid response: {code}').format(code=response.status_code))
|
form.add_error('url', _('Invalid response: {code}').format(code=response.status_code))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@ -24,6 +21,7 @@ from plugin.registry import registry
|
|||||||
|
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
from plugin.base.label import label as plugin_label
|
||||||
|
|
||||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||||
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
|
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
|
||||||
@ -156,7 +154,7 @@ class LabelPrintMixin:
|
|||||||
|
|
||||||
# Offload a background task to print the provided label
|
# Offload a background task to print the provided label
|
||||||
offload_task(
|
offload_task(
|
||||||
'plugin.events.print_label',
|
plugin_label.print_label,
|
||||||
plugin.plugin_slug(),
|
plugin.plugin_slug(),
|
||||||
image,
|
image,
|
||||||
label_instance=label_instance,
|
label_instance=label_instance,
|
||||||
|
@ -105,7 +105,7 @@ class LabelConfig(AppConfig):
|
|||||||
# File already exists - let's see if it is the "same",
|
# File already exists - let's see if it is the "same",
|
||||||
# or if we need to overwrite it with a newer copy!
|
# or if we need to overwrite it with a newer copy!
|
||||||
|
|
||||||
if not hashFile(dst_file) == hashFile(src_file): # pragma: no cover
|
if hashFile(dst_file) != hashFile(src_file): # pragma: no cover
|
||||||
logger.info(f"Hash differs for '{filename}'")
|
logger.info(f"Hash differs for '{filename}'")
|
||||||
to_copy = True
|
to_copy = True
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ class LabelConfig(AppConfig):
|
|||||||
# File already exists - let's see if it is the "same",
|
# File already exists - let's see if it is the "same",
|
||||||
# or if we need to overwrite it with a newer copy!
|
# or if we need to overwrite it with a newer copy!
|
||||||
|
|
||||||
if not hashFile(dst_file) == hashFile(src_file): # pragma: no cover
|
if hashFile(dst_file) != hashFile(src_file): # pragma: no cover
|
||||||
logger.info(f"Hash differs for '{filename}'")
|
logger.info(f"Hash differs for '{filename}'")
|
||||||
to_copy = True
|
to_copy = True
|
||||||
|
|
||||||
@ -291,7 +291,7 @@ class LabelConfig(AppConfig):
|
|||||||
if os.path.exists(dst_file):
|
if os.path.exists(dst_file):
|
||||||
# File already exists - let's see if it is the "same"
|
# File already exists - let's see if it is the "same"
|
||||||
|
|
||||||
if not hashFile(dst_file) == hashFile(src_file): # pragma: no cover
|
if hashFile(dst_file) != hashFile(src_file): # pragma: no cover
|
||||||
logger.info(f"Hash differs for '{filename}'")
|
logger.info(f"Hash differs for '{filename}'")
|
||||||
to_copy = True
|
to_copy = True
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Label printing models
|
Label printing models
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
# Tests for labels
|
# Tests for labels
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
# Tests for labels
|
# Tests for labels
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
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
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
JSON API for the Order app
|
JSON API for the Order app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
|
|
||||||
@ -27,6 +24,8 @@ import order.serializers as serializers
|
|||||||
from part.models import Part
|
from part.models import Part
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
|
from plugin.serializers import MetadataSerializer
|
||||||
|
|
||||||
|
|
||||||
class GeneralExtraLineList:
|
class GeneralExtraLineList:
|
||||||
"""
|
"""
|
||||||
@ -347,6 +346,15 @@ class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView):
|
|||||||
serializer_class = serializers.PurchaseOrderIssueSerializer
|
serializer_class = serializers.PurchaseOrderIssueSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderMetadata(generics.RetrieveUpdateAPIView):
|
||||||
|
"""API endpoint for viewing / updating PurchaseOrder metadata"""
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
return MetadataSerializer(models.PurchaseOrder, *args, **kwargs)
|
||||||
|
|
||||||
|
queryset = models.PurchaseOrder.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderReceive(PurchaseOrderContextMixin, generics.CreateAPIView):
|
class PurchaseOrderReceive(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint to receive stock items against a purchase order.
|
API endpoint to receive stock items against a purchase order.
|
||||||
@ -916,6 +924,15 @@ class SalesOrderComplete(SalesOrderContextMixin, generics.CreateAPIView):
|
|||||||
serializer_class = serializers.SalesOrderCompleteSerializer
|
serializer_class = serializers.SalesOrderCompleteSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderMetadata(generics.RetrieveUpdateAPIView):
|
||||||
|
"""API endpoint for viewing / updating SalesOrder metadata"""
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
return MetadataSerializer(models.SalesOrder, *args, **kwargs)
|
||||||
|
|
||||||
|
queryset = models.SalesOrder.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocateSerials(SalesOrderContextMixin, generics.CreateAPIView):
|
class SalesOrderAllocateSerials(SalesOrderContextMixin, generics.CreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint to allocation stock items against a SalesOrder,
|
API endpoint to allocation stock items against a SalesOrder,
|
||||||
@ -1138,10 +1155,13 @@ order_api_urls = [
|
|||||||
|
|
||||||
# Individual purchase order detail URLs
|
# Individual purchase order detail URLs
|
||||||
re_path(r'^(?P<pk>\d+)/', include([
|
re_path(r'^(?P<pk>\d+)/', include([
|
||||||
re_path(r'^issue/', PurchaseOrderIssue.as_view(), name='api-po-issue'),
|
|
||||||
re_path(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'),
|
|
||||||
re_path(r'^cancel/', PurchaseOrderCancel.as_view(), name='api-po-cancel'),
|
re_path(r'^cancel/', PurchaseOrderCancel.as_view(), name='api-po-cancel'),
|
||||||
re_path(r'^complete/', PurchaseOrderComplete.as_view(), name='api-po-complete'),
|
re_path(r'^complete/', PurchaseOrderComplete.as_view(), name='api-po-complete'),
|
||||||
|
re_path(r'^issue/', PurchaseOrderIssue.as_view(), name='api-po-issue'),
|
||||||
|
re_path(r'^metadata/', PurchaseOrderMetadata.as_view(), name='api-po-metadata'),
|
||||||
|
re_path(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'),
|
||||||
|
|
||||||
|
# PurchaseOrder detail API endpoint
|
||||||
re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
|
re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
@ -1178,10 +1198,13 @@ order_api_urls = [
|
|||||||
|
|
||||||
# Sales order detail view
|
# Sales order detail view
|
||||||
re_path(r'^(?P<pk>\d+)/', include([
|
re_path(r'^(?P<pk>\d+)/', include([
|
||||||
re_path(r'^cancel/', SalesOrderCancel.as_view(), name='api-so-cancel'),
|
|
||||||
re_path(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
|
|
||||||
re_path(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
|
re_path(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
|
||||||
re_path(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),
|
re_path(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),
|
||||||
|
re_path(r'^cancel/', SalesOrderCancel.as_view(), name='api-so-cancel'),
|
||||||
|
re_path(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
|
||||||
|
re_path(r'^metadata/', SalesOrderMetadata.as_view(), name='api-so-metadata'),
|
||||||
|
|
||||||
|
# SalesOrder detail endpoint
|
||||||
re_path(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'),
|
re_path(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Django Forms for interacting with Order objects
|
Django Forms for interacting with Order objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
23
InvenTree/order/migrations/0067_auto_20220516_1120.py
Normal file
23
InvenTree/order/migrations/0067_auto_20220516_1120.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.13 on 2022-05-16 11:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0066_alter_purchaseorder_supplier'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||||
|
),
|
||||||
|
]
|
@ -30,7 +30,9 @@ from users import models as UserModels
|
|||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
from stock import models as stock_models
|
from stock import models as stock_models
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
|
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
|
from plugin.models import MetadataMixin
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
|
from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
|
||||||
@ -97,7 +99,7 @@ def get_next_so_number():
|
|||||||
return reference
|
return reference
|
||||||
|
|
||||||
|
|
||||||
class Order(ReferenceIndexingMixin):
|
class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||||
""" Abstract model for an order.
|
""" Abstract model for an order.
|
||||||
|
|
||||||
Instances of this class:
|
Instances of this class:
|
||||||
@ -306,7 +308,7 @@ class PurchaseOrder(Order):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError({'quantity': _("Invalid quantity provided")})
|
raise ValidationError({'quantity': _("Invalid quantity provided")})
|
||||||
|
|
||||||
if not supplier_part.supplier == self.supplier:
|
if supplier_part.supplier != self.supplier:
|
||||||
raise ValidationError({'supplier': _("Part supplier must match PO supplier")})
|
raise ValidationError({'supplier': _("Part supplier must match PO supplier")})
|
||||||
|
|
||||||
if group:
|
if group:
|
||||||
@ -445,7 +447,7 @@ class PurchaseOrder(Order):
|
|||||||
if barcode is None:
|
if barcode is None:
|
||||||
barcode = ''
|
barcode = ''
|
||||||
|
|
||||||
if not self.status == PurchaseOrderStatus.PLACED:
|
if self.status != PurchaseOrderStatus.PLACED:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"Lines can only be received against an order marked as 'PLACED'"
|
"Lines can only be received against an order marked as 'PLACED'"
|
||||||
)
|
)
|
||||||
@ -729,7 +731,7 @@ class SalesOrder(Order):
|
|||||||
Return True if this order can be cancelled
|
Return True if this order can be cancelled
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.status == SalesOrderStatus.PENDING:
|
if self.status != SalesOrderStatus.PENDING:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -1295,7 +1297,7 @@ class SalesOrderAllocation(models.Model):
|
|||||||
raise ValidationError({'item': _('Stock item has not been assigned')})
|
raise ValidationError({'item': _('Stock item has not been assigned')})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.line.part == self.item.part:
|
if self.line.part != self.item.part:
|
||||||
errors['item'] = _('Cannot allocate stock item to a line with a different part')
|
errors['item'] = _('Cannot allocate stock item to a line with a different part')
|
||||||
except PartModels.Part.DoesNotExist:
|
except PartModels.Part.DoesNotExist:
|
||||||
errors['line'] = _('Cannot allocate stock to a line without a part')
|
errors['line'] = _('Cannot allocate stock to a line without a part')
|
||||||
@ -1310,7 +1312,7 @@ class SalesOrderAllocation(models.Model):
|
|||||||
if self.quantity <= 0:
|
if self.quantity <= 0:
|
||||||
errors['quantity'] = _('Allocation quantity must be greater than zero')
|
errors['quantity'] = _('Allocation quantity must be greater than zero')
|
||||||
|
|
||||||
if self.item.serial and not self.quantity == 1:
|
if self.item.serial and self.quantity != 1:
|
||||||
errors['quantity'] = _('Quantity must be 1 for serialized stock item')
|
errors['quantity'] = _('Quantity must be 1 for serialized stock item')
|
||||||
|
|
||||||
if self.line.order != self.shipment.order:
|
if self.line.order != self.shipment.order:
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
JSON serializers for the Order API
|
JSON serializers for the Order API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -306,6 +306,22 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
|
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
|
||||||
|
|
||||||
|
def test_po_metadata(self):
|
||||||
|
url = reverse('api-po-metadata', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
self.patch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'metadata': {
|
||||||
|
'yam': 'yum',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
order = models.PurchaseOrder.objects.get(pk=1)
|
||||||
|
self.assertEqual(order.get_metadata('yam'), 'yum')
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderReceiveTest(OrderTest):
|
class PurchaseOrderReceiveTest(OrderTest):
|
||||||
"""
|
"""
|
||||||
@ -875,6 +891,22 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
self.assertEqual(so.status, SalesOrderStatus.CANCELLED)
|
self.assertEqual(so.status, SalesOrderStatus.CANCELLED)
|
||||||
|
|
||||||
|
def test_so_metadata(self):
|
||||||
|
url = reverse('api-so-metadata', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
self.patch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'metadata': {
|
||||||
|
'xyz': 'abc',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
order = models.SalesOrder.objects.get(pk=1)
|
||||||
|
self.assertEqual(order.get_metadata('xyz'), 'abc')
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocateTest(OrderTest):
|
class SalesOrderAllocateTest(OrderTest):
|
||||||
"""
|
"""
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
""" Unit tests for Order views (see views.py) """
|
""" Unit tests for Order views (see views.py) """
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Django views for interacting with Order app
|
Django views for interacting with Order app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Provides a JSON API for the Part app
|
Provides a JSON API for the Part app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
@ -44,6 +41,7 @@ from stock.models import StockItem, StockLocation
|
|||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from build.models import Build, BuildItem
|
from build.models import Build, BuildItem
|
||||||
import order.models
|
import order.models
|
||||||
|
from plugin.serializers import MetadataSerializer
|
||||||
|
|
||||||
from . import serializers as part_serializers
|
from . import serializers as part_serializers
|
||||||
|
|
||||||
@ -203,6 +201,15 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryMetadata(generics.RetrieveUpdateAPIView):
|
||||||
|
"""API endpoint for viewing / updating PartCategory metadata"""
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
return MetadataSerializer(PartCategory, *args, **kwargs)
|
||||||
|
|
||||||
|
queryset = PartCategory.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class CategoryParameterList(generics.ListAPIView):
|
class CategoryParameterList(generics.ListAPIView):
|
||||||
""" API endpoint for accessing a list of PartCategoryParameterTemplate objects.
|
""" API endpoint for accessing a list of PartCategoryParameterTemplate objects.
|
||||||
|
|
||||||
@ -587,6 +594,17 @@ class PartScheduling(generics.RetrieveAPIView):
|
|||||||
return Response(schedule)
|
return Response(schedule)
|
||||||
|
|
||||||
|
|
||||||
|
class PartMetadata(generics.RetrieveUpdateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for viewing / updating Part metadata
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
return MetadataSerializer(Part, *args, **kwargs)
|
||||||
|
|
||||||
|
queryset = Part.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class PartSerialNumberDetail(generics.RetrieveAPIView):
|
class PartSerialNumberDetail(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for returning extra serial number information about a particular part
|
API endpoint for returning extra serial number information about a particular part
|
||||||
@ -1912,7 +1930,15 @@ part_api_urls = [
|
|||||||
re_path(r'^tree/', CategoryTree.as_view(), name='api-part-category-tree'),
|
re_path(r'^tree/', CategoryTree.as_view(), name='api-part-category-tree'),
|
||||||
re_path(r'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
|
re_path(r'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
|
||||||
|
|
||||||
re_path(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
|
# Category detail endpoints
|
||||||
|
re_path(r'^(?P<pk>\d+)/', include([
|
||||||
|
|
||||||
|
re_path(r'^metadata/', CategoryMetadata.as_view(), name='api-part-category-metadata'),
|
||||||
|
|
||||||
|
# PartCategory detail endpoint
|
||||||
|
re_path(r'^.*$', CategoryDetail.as_view(), name='api-part-category-detail'),
|
||||||
|
])),
|
||||||
|
|
||||||
path('', CategoryList.as_view(), name='api-part-category-list'),
|
path('', CategoryList.as_view(), name='api-part-category-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
@ -1973,6 +1999,9 @@ part_api_urls = [
|
|||||||
# Endpoint for validating a BOM for the specific Part
|
# Endpoint for validating a BOM for the specific Part
|
||||||
re_path(r'^bom-validate/', PartValidateBOM.as_view(), name='api-part-bom-validate'),
|
re_path(r'^bom-validate/', PartValidateBOM.as_view(), name='api-part-bom-validate'),
|
||||||
|
|
||||||
|
# Part metadata
|
||||||
|
re_path(r'^metadata/', PartMetadata.as_view(), name='api-part-metadata'),
|
||||||
|
|
||||||
# Part detail endpoint
|
# Part detail endpoint
|
||||||
re_path(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
|
re_path(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
|
||||||
])),
|
])),
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
Django Forms for interacting with Part objects
|
Django Forms for interacting with Part objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user