mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into theme-tests
This commit is contained in:
commit
262409eba9
3
.github/workflows/qc_checks.yaml
vendored
3
.github/workflows/qc_checks.yaml
vendored
@ -196,6 +196,7 @@ jobs:
|
|||||||
name: Postgres
|
name: Postgres
|
||||||
needs: ['javascript', 'html']
|
needs: ['javascript', 'html']
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
INVENTREE_DB_ENGINE: django.db.backends.postgresql
|
INVENTREE_DB_ENGINE: django.db.backends.postgresql
|
||||||
@ -253,6 +254,8 @@ jobs:
|
|||||||
name: MySql
|
name: MySql
|
||||||
needs: ['javascript', 'html']
|
needs: ['javascript', 'html']
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Database backend configuration
|
# Database backend configuration
|
||||||
INVENTREE_DB_ENGINE: django.db.backends.mysql
|
INVENTREE_DB_ENGINE: django.db.backends.mysql
|
||||||
|
@ -4,11 +4,15 @@ InvenTree API version information
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 46
|
INVENTREE_API_VERSION = 47
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
|
v47 -> 2022-05-10 : https://github.com/inventree/InvenTree/pull/2964
|
||||||
|
- Fixes barcode API error response when scanning a StockItem which does not exist
|
||||||
|
- Fixes barcode API error response when scanning a StockLocation which does not exist
|
||||||
|
|
||||||
v46 -> 2022-05-09
|
v46 -> 2022-05-09
|
||||||
- Fixes read permissions on settings API
|
- Fixes read permissions on settings API
|
||||||
- Allows non-staff users to read global settings via the API
|
- Allows non-staff users to read global settings via the API
|
||||||
|
@ -190,8 +190,11 @@ class InvenTreeConfig(AppConfig):
|
|||||||
user = get_user_model()
|
user = get_user_model()
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
new_user = user.objects.create_superuser(add_user, add_email, add_password)
|
if user.objects.filter(username=add_user).exists():
|
||||||
logger.info(f'User {str(new_user)} was created!')
|
logger.info(f"User {add_user} already exists - skipping creation")
|
||||||
|
else:
|
||||||
|
new_user = user.objects.create_superuser(add_user, add_email, add_password)
|
||||||
|
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:
|
if settings.TESTING_ENV:
|
||||||
|
@ -900,7 +900,7 @@ PLUGINS_ENABLED = _is_true(get_setting(
|
|||||||
PLUGIN_FILE = get_plugin_file()
|
PLUGIN_FILE = get_plugin_file()
|
||||||
|
|
||||||
# Plugin Directories (local plugins will be loaded from these directories)
|
# Plugin Directories (local plugins will be loaded from these directories)
|
||||||
PLUGIN_DIRS = ['plugin.builtin', 'barcodes.plugins', ]
|
PLUGIN_DIRS = ['plugin.builtin', ]
|
||||||
|
|
||||||
if not TESTING:
|
if not TESTING:
|
||||||
# load local deploy directory in prod
|
# load local deploy directory in prod
|
||||||
|
@ -18,7 +18,6 @@ from build.urls import build_urls
|
|||||||
from order.urls import order_urls
|
from order.urls import order_urls
|
||||||
from plugin.urls import get_plugin_urls
|
from plugin.urls import get_plugin_urls
|
||||||
|
|
||||||
from barcodes.api import barcode_api_urls
|
|
||||||
from common.api import common_api_urls, settings_api_urls
|
from common.api import common_api_urls, settings_api_urls
|
||||||
from part.api import part_api_urls, bom_api_urls
|
from part.api import part_api_urls, bom_api_urls
|
||||||
from company.api import company_api_urls
|
from company.api import company_api_urls
|
||||||
@ -28,6 +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 django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
@ -59,7 +59,6 @@ if settings.PLUGINS_ENABLED:
|
|||||||
)
|
)
|
||||||
|
|
||||||
apipatterns += [
|
apipatterns += [
|
||||||
re_path(r'^barcode/', include(barcode_api_urls)),
|
|
||||||
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)),
|
||||||
@ -75,6 +74,7 @@ apipatterns += [
|
|||||||
|
|
||||||
# Plugin endpoints
|
# Plugin endpoints
|
||||||
re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
|
re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
|
||||||
|
re_path(r'^barcode/', include(barcode_api_urls)),
|
||||||
|
|
||||||
# Webhook enpoint
|
# Webhook enpoint
|
||||||
path('', include(common_api_urls)),
|
path('', include(common_api_urls)),
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import plugin.builtin.barcode.mixins as mixin
|
|
||||||
import plugin.integration
|
|
||||||
|
|
||||||
|
|
||||||
hash_barcode = mixin.hash_barcode
|
|
||||||
|
|
||||||
|
|
||||||
class BarcodePlugin(mixin.BarcodeMixin, plugin.integration.IntegrationPluginBase):
|
|
||||||
"""
|
|
||||||
Legacy barcode plugin definition - will be replaced
|
|
||||||
Please use the new Integration Plugin API and the BarcodeMixin
|
|
||||||
"""
|
|
||||||
# TODO @matmair remove this with InvenTree 0.7.0
|
|
||||||
def __init__(self, barcode_data=None):
|
|
||||||
warnings.warn("using the BarcodePlugin is depreceated", DeprecationWarning)
|
|
||||||
super().__init__()
|
|
||||||
self.init(barcode_data)
|
|
@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
DigiKey barcode decoding
|
|
||||||
"""
|
|
||||||
|
|
||||||
from barcodes.barcode import BarcodePlugin
|
|
||||||
|
|
||||||
|
|
||||||
class DigikeyBarcodePlugin(BarcodePlugin):
|
|
||||||
|
|
||||||
PLUGIN_NAME = "DigikeyBarcode"
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
"""
|
|
||||||
TODO: Validation of Digikey barcodes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return False
|
|
@ -265,6 +265,13 @@ class LabelConfig(AppConfig):
|
|||||||
'width': 70,
|
'width': 70,
|
||||||
'height': 24,
|
'height': 24,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'file': 'part_label_code128.html',
|
||||||
|
'name': 'Barcode Part Label',
|
||||||
|
'description': 'Simple part label with Code128 barcode',
|
||||||
|
'width': 70,
|
||||||
|
'height': 24,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for label in labels:
|
for label in labels:
|
||||||
|
33
InvenTree/label/templates/label/part/part_label_code128.html
Normal file
33
InvenTree/label/templates/label/part/part_label_code128.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends "label/label_base.html" %}
|
||||||
|
|
||||||
|
{% load barcode %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
|
||||||
|
.qr {
|
||||||
|
position: fixed;
|
||||||
|
left: 0mm;
|
||||||
|
top: 0mm;
|
||||||
|
height: {{ height }}mm;
|
||||||
|
width: {{ height }}mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
display: inline;
|
||||||
|
position: absolute;
|
||||||
|
left: {{ height }}mm;
|
||||||
|
top: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<img class='qr' src='{% barcode qr_data %}'>
|
||||||
|
|
||||||
|
<div class='part'>
|
||||||
|
{{ part.full_name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -5,20 +5,29 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.urls import reverse
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from InvenTree.helpers import validateFilterString
|
from InvenTree.helpers import validateFilterString
|
||||||
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
|
|
||||||
from .models import StockItemLabel, StockLocationLabel
|
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
|
|
||||||
class LabelTest(TestCase):
|
class LabelTest(InvenTreeAPITestCase):
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
'stock'
|
||||||
|
]
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
# ensure the labels were created
|
# ensure the labels were created
|
||||||
apps.get_app_config('label').create_labels()
|
apps.get_app_config('label').create_labels()
|
||||||
|
|
||||||
@ -77,3 +86,13 @@ class LabelTest(TestCase):
|
|||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
validateFilterString(bad_filter_string, model=StockItem)
|
validateFilterString(bad_filter_string, model=StockItem)
|
||||||
|
|
||||||
|
def test_label_rendering(self):
|
||||||
|
"""Test label rendering"""
|
||||||
|
|
||||||
|
labels = PartLabel.objects.all()
|
||||||
|
part = PartLabel.objects.first()
|
||||||
|
|
||||||
|
for label in labels:
|
||||||
|
url = reverse('api-part-label-print', kwargs={'pk': label.pk})
|
||||||
|
self.get(f'{url}?parts={part.pk}', expected_code=200)
|
||||||
|
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
@ -49,6 +49,8 @@ from InvenTree import validators
|
|||||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment, DataImportMixin
|
from InvenTree.models import InvenTreeTree, InvenTreeAttachment, DataImportMixin
|
||||||
from InvenTree.fields import InvenTreeURLField
|
from InvenTree.fields import InvenTreeURLField
|
||||||
from InvenTree.helpers import decimal2string, normalize, decimal2money
|
from InvenTree.helpers import decimal2string, normalize, decimal2money
|
||||||
|
|
||||||
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||||
@ -2292,7 +2294,7 @@ def after_save_part(sender, instance: Part, created, **kwargs):
|
|||||||
Function to be executed after a Part is saved
|
Function to be executed after a Part is saved
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not created:
|
if not created and not InvenTree.ready.isImportingData():
|
||||||
# Check part stock only if we are *updating* the part (not creating it)
|
# Check part stock only if we are *updating* the part (not creating it)
|
||||||
|
|
||||||
# Run this check in the background
|
# Run this check in the background
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.urls import reverse, path, re_path
|
||||||
from django.urls import reverse
|
|
||||||
from django.urls import path, re_path
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
@ -12,8 +10,8 @@ from rest_framework.views import APIView
|
|||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
from stock.serializers import StockItemSerializer
|
from stock.serializers import StockItemSerializer
|
||||||
|
|
||||||
from barcodes.plugins.inventree_barcode import InvenTreeBarcodePlugin
|
from plugin.builtin.barcodes.inventree_barcode import InvenTreeBarcodePlugin
|
||||||
from barcodes.barcode import hash_barcode
|
from plugin.builtin.barcodes.mixins import hash_barcode
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
|
|
@ -13,7 +13,8 @@ references model objects actually exist in the database.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from barcodes.barcode import BarcodePlugin
|
from plugin import IntegrationPluginBase
|
||||||
|
from plugin.mixins import BarcodeMixin
|
||||||
|
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@ -21,7 +22,7 @@ from part.models import Part
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeBarcodePlugin(BarcodePlugin):
|
class InvenTreeBarcodePlugin(BarcodeMixin, IntegrationPluginBase):
|
||||||
|
|
||||||
PLUGIN_NAME = "InvenTreeBarcode"
|
PLUGIN_NAME = "InvenTreeBarcode"
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
item = StockItem.objects.get(pk=pk)
|
item = StockItem.objects.get(pk=pk)
|
||||||
return item
|
return item
|
||||||
except (ValueError, StockItem.DoesNotExist): # pragma: no cover
|
except (ValueError, StockItem.DoesNotExist): # pragma: no cover
|
||||||
raise ValidationError({k, "Stock item does not exist"})
|
raise ValidationError({k: "Stock item does not exist"})
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -111,7 +112,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
loc = StockLocation.objects.get(pk=pk)
|
loc = StockLocation.objects.get(pk=pk)
|
||||||
return loc
|
return loc
|
||||||
except (ValueError, StockLocation.DoesNotExist): # pragma: no cover
|
except (ValueError, StockLocation.DoesNotExist): # pragma: no cover
|
||||||
raise ValidationError({k, "Stock location does not exist"})
|
raise ValidationError({k: "Stock location does not exist"})
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -132,12 +133,12 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
try:
|
try:
|
||||||
pk = self.data[k]['id']
|
pk = self.data[k]['id']
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
raise ValidationError({k, 'id parameter not supplied'})
|
raise ValidationError({k: 'id parameter not supplied'})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
part = Part.objects.get(pk=pk)
|
part = Part.objects.get(pk=pk)
|
||||||
return part
|
return part
|
||||||
except (ValueError, Part.DoesNotExist): # pragma: no cover
|
except (ValueError, Part.DoesNotExist): # pragma: no cover
|
||||||
raise ValidationError({k, 'Part does not exist'})
|
raise ValidationError({k: 'Part does not exist'})
|
||||||
|
|
||||||
return None
|
return None
|
62
InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
Normal file
62
InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Unit tests for InvenTreeBarcodePlugin"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
|
class TestInvenTreeBarcode(APITestCase):
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
'stock'
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Create a user for auth
|
||||||
|
user = get_user_model()
|
||||||
|
user.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||||
|
|
||||||
|
self.client.login(username='testuser', password='password')
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
"""
|
||||||
|
Test all possible error cases for assigment action
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_assert_error(barcode_data):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('api-barcode-link'), format='json',
|
||||||
|
data={
|
||||||
|
'barcode': barcode_data,
|
||||||
|
'stockitem': 521
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('error', response.data)
|
||||||
|
|
||||||
|
# test with already existing stock
|
||||||
|
test_assert_error('{"stockitem": 521}')
|
||||||
|
|
||||||
|
# test with already existing stock location
|
||||||
|
test_assert_error('{"stocklocation": 7}')
|
||||||
|
|
||||||
|
# test with already existing part location
|
||||||
|
test_assert_error('{"part": 10004}')
|
||||||
|
|
||||||
|
# test with hash
|
||||||
|
test_assert_error('{"blbla": 10004}')
|
||||||
|
|
||||||
|
def test_scan(self):
|
||||||
|
"""
|
||||||
|
Test that a barcode can be scanned
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.post(reverse('api-barcode-scan'), format='json', data={'barcode': 'blbla=10004'})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('success', response.data)
|
@ -17,7 +17,7 @@ from django.dispatch.dispatcher import receiver
|
|||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
import common.notifications
|
import common.notifications
|
||||||
|
|
||||||
from InvenTree.ready import canAppAccessDatabase
|
from InvenTree.ready import canAppAccessDatabase, isImportingData
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
@ -113,6 +113,10 @@ def allow_table_event(table_name):
|
|||||||
We *do not* want events to be fired for some tables!
|
We *do not* want events to be fired for some tables!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if isImportingData():
|
||||||
|
# Prevent table events during the data import process
|
||||||
|
return False
|
||||||
|
|
||||||
table_name = table_name.lower().strip()
|
table_name = table_name.lower().strip()
|
||||||
|
|
||||||
# Ignore any tables which start with these prefixes
|
# Ignore any tables which start with these prefixes
|
||||||
|
@ -7,7 +7,7 @@ from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMi
|
|||||||
from common.notifications import SingleNotificationMethod, BulkNotificationMethod
|
from common.notifications import SingleNotificationMethod, BulkNotificationMethod
|
||||||
|
|
||||||
from ..builtin.action.mixins import ActionMixin
|
from ..builtin.action.mixins import ActionMixin
|
||||||
from ..builtin.barcode.mixins import BarcodeMixin
|
from ..builtin.barcodes.mixins import BarcodeMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'APICallMixin',
|
'APICallMixin',
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
|
|
||||||
<em>This location has no sublocations!</em>
|
<em>This location has no sublocations!</em>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>Location Name</b>: {{ location.name }}</li>
|
<li><strong>Location Name</strong>: {{ location.name }}</li>
|
||||||
<li><b>Location Path</b>: {{ location.pathstring }}</li>
|
<li><strong>Location Path</strong>: {{ location.pathstring }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -86,6 +86,22 @@ class BarcodeAPITest(APITestCase):
|
|||||||
self.assertIn('barcode_data', response.data)
|
self.assertIn('barcode_data', response.data)
|
||||||
self.assertEqual(response.data['part']['pk'], 1)
|
self.assertEqual(response.data['part']['pk'], 1)
|
||||||
|
|
||||||
|
def test_invalid_part(self):
|
||||||
|
"""Test response for invalid part"""
|
||||||
|
response = self.client.post(
|
||||||
|
self.scan_url,
|
||||||
|
{
|
||||||
|
'barcode': {
|
||||||
|
'part': 999999999,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['part'], 'Part does not exist')
|
||||||
|
|
||||||
def test_find_stock_item(self):
|
def test_find_stock_item(self):
|
||||||
"""
|
"""
|
||||||
Test that we can lookup a stock item based on ID
|
Test that we can lookup a stock item based on ID
|
||||||
@ -106,6 +122,23 @@ class BarcodeAPITest(APITestCase):
|
|||||||
self.assertIn('barcode_data', response.data)
|
self.assertIn('barcode_data', response.data)
|
||||||
self.assertEqual(response.data['stockitem']['pk'], 1)
|
self.assertEqual(response.data['stockitem']['pk'], 1)
|
||||||
|
|
||||||
|
def test_invalid_item(self):
|
||||||
|
"""Test response for invalid stock item"""
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.scan_url,
|
||||||
|
{
|
||||||
|
'barcode': {
|
||||||
|
'stockitem': 999999999,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['stockitem'], 'Stock item does not exist')
|
||||||
|
|
||||||
def test_find_location(self):
|
def test_find_location(self):
|
||||||
"""
|
"""
|
||||||
Test that we can lookup a stock location based on ID
|
Test that we can lookup a stock location based on ID
|
||||||
@ -126,6 +159,23 @@ class BarcodeAPITest(APITestCase):
|
|||||||
self.assertIn('barcode_data', response.data)
|
self.assertIn('barcode_data', response.data)
|
||||||
self.assertEqual(response.data['stocklocation']['pk'], 1)
|
self.assertEqual(response.data['stocklocation']['pk'], 1)
|
||||||
|
|
||||||
|
def test_invalid_location(self):
|
||||||
|
"""Test response for an invalid location"""
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.scan_url,
|
||||||
|
{
|
||||||
|
'barcode': {
|
||||||
|
'stocklocation': 999999999,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['stocklocation'], 'Stock location does not exist')
|
||||||
|
|
||||||
def test_integer_barcode(self):
|
def test_integer_barcode(self):
|
||||||
|
|
||||||
response = self.postBarcode(self.scan_url, '123456789')
|
response = self.postBarcode(self.scan_url, '123456789')
|
||||||
@ -214,57 +264,3 @@ class BarcodeAPITest(APITestCase):
|
|||||||
|
|
||||||
self.assertIn('error', data)
|
self.assertIn('error', data)
|
||||||
self.assertNotIn('success', data)
|
self.assertNotIn('success', data)
|
||||||
|
|
||||||
|
|
||||||
class TestInvenTreeBarcode(APITestCase):
|
|
||||||
|
|
||||||
fixtures = [
|
|
||||||
'category',
|
|
||||||
'part',
|
|
||||||
'location',
|
|
||||||
'stock'
|
|
||||||
]
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# Create a user for auth
|
|
||||||
user = get_user_model()
|
|
||||||
user.objects.create_user('testuser', 'test@testing.com', 'password')
|
|
||||||
|
|
||||||
self.client.login(username='testuser', password='password')
|
|
||||||
|
|
||||||
def test_errors(self):
|
|
||||||
"""
|
|
||||||
Test all possible error cases for assigment action
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_assert_error(barcode_data):
|
|
||||||
response = self.client.post(
|
|
||||||
reverse('api-barcode-link'), format='json',
|
|
||||||
data={
|
|
||||||
'barcode': barcode_data,
|
|
||||||
'stockitem': 521
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertIn('error', response.data)
|
|
||||||
|
|
||||||
# test with already existing stock
|
|
||||||
test_assert_error('{"stockitem": 521}')
|
|
||||||
|
|
||||||
# test with already existing stock location
|
|
||||||
test_assert_error('{"stocklocation": 7}')
|
|
||||||
|
|
||||||
# test with already existing part location
|
|
||||||
test_assert_error('{"part": 10004}')
|
|
||||||
|
|
||||||
# test with hash
|
|
||||||
test_assert_error('{"blbla": 10004}')
|
|
||||||
|
|
||||||
def test_scan(self):
|
|
||||||
"""
|
|
||||||
Test that a barcode can be scanned
|
|
||||||
"""
|
|
||||||
|
|
||||||
response = self.client.post(reverse('api-barcode-scan'), format='json', data={'barcode': 'blbla=10004'})
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertIn('success', response.data)
|
|
@ -30,7 +30,8 @@ from mptt.managers import TreeManager
|
|||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from InvenTree import helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
@ -137,7 +138,7 @@ class StockLocation(InvenTreeTree):
|
|||||||
def format_barcode(self, **kwargs):
|
def format_barcode(self, **kwargs):
|
||||||
""" Return a JSON string for formatting a barcode for this StockLocation object """
|
""" Return a JSON string for formatting a barcode for this StockLocation object """
|
||||||
|
|
||||||
return helpers.MakeBarcode(
|
return InvenTree.helpers.MakeBarcode(
|
||||||
'stocklocation',
|
'stocklocation',
|
||||||
self.pk,
|
self.pk,
|
||||||
{
|
{
|
||||||
@ -577,7 +578,7 @@ class StockItem(MPTTModel):
|
|||||||
Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change)
|
Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return helpers.MakeBarcode(
|
return InvenTree.helpers.MakeBarcode(
|
||||||
"stockitem",
|
"stockitem",
|
||||||
self.id,
|
self.id,
|
||||||
{
|
{
|
||||||
@ -1775,7 +1776,7 @@ class StockItem(MPTTModel):
|
|||||||
sn=self.serial)
|
sn=self.serial)
|
||||||
else:
|
else:
|
||||||
s = '{n} x {part}'.format(
|
s = '{n} x {part}'.format(
|
||||||
n=helpers.decimal2string(self.quantity),
|
n=InvenTree.helpers.decimal2string(self.quantity),
|
||||||
part=self.part.full_name)
|
part=self.part.full_name)
|
||||||
|
|
||||||
if self.location:
|
if self.location:
|
||||||
@ -1783,7 +1784,7 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
if self.purchase_order:
|
if self.purchase_order:
|
||||||
s += " ({pre}{po})".format(
|
s += " ({pre}{po})".format(
|
||||||
pre=helpers.getSetting("PURCHASEORDER_REFERENCE_PREFIX"),
|
pre=InvenTree.helpers.getSetting("PURCHASEORDER_REFERENCE_PREFIX"),
|
||||||
po=self.purchase_order,
|
po=self.purchase_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1851,7 +1852,7 @@ class StockItem(MPTTModel):
|
|||||||
result_map = {}
|
result_map = {}
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
key = helpers.generateTestKey(result.test)
|
key = InvenTree.helpers.generateTestKey(result.test)
|
||||||
result_map[key] = result
|
result_map[key] = result
|
||||||
|
|
||||||
# Do we wish to "cascade" and include test results from installed stock items?
|
# Do we wish to "cascade" and include test results from installed stock items?
|
||||||
@ -1898,7 +1899,7 @@ class StockItem(MPTTModel):
|
|||||||
failed = 0
|
failed = 0
|
||||||
|
|
||||||
for test in required:
|
for test in required:
|
||||||
key = helpers.generateTestKey(test.test_name)
|
key = InvenTree.helpers.generateTestKey(test.test_name)
|
||||||
|
|
||||||
if key in results:
|
if key in results:
|
||||||
result = results[key]
|
result = results[key]
|
||||||
@ -1949,7 +1950,7 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
# Attempt to validate report filter (skip if invalid)
|
# Attempt to validate report filter (skip if invalid)
|
||||||
try:
|
try:
|
||||||
filters = helpers.validateFilterString(test_report.filters)
|
filters = InvenTree.helpers.validateFilterString(test_report.filters)
|
||||||
if item_query.filter(**filters).exists():
|
if item_query.filter(**filters).exists():
|
||||||
reports.append(test_report)
|
reports.append(test_report)
|
||||||
except (ValidationError, FieldError):
|
except (ValidationError, FieldError):
|
||||||
@ -1977,7 +1978,7 @@ class StockItem(MPTTModel):
|
|||||||
for lbl in label.models.StockItemLabel.objects.filter(enabled=True):
|
for lbl in label.models.StockItemLabel.objects.filter(enabled=True):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filters = helpers.validateFilterString(lbl.filters)
|
filters = InvenTree.helpers.validateFilterString(lbl.filters)
|
||||||
|
|
||||||
if item_query.filter(**filters).exists():
|
if item_query.filter(**filters).exists():
|
||||||
labels.append(lbl)
|
labels.append(lbl)
|
||||||
@ -2016,8 +2017,9 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
|
|||||||
Function to be executed after a StockItem object is deleted
|
Function to be executed after a StockItem object is deleted
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Run this check in the background
|
if not InvenTree.ready.isImportingData():
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
# Run this check in the background
|
||||||
|
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
|
@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
|
||||||
@ -2026,8 +2028,9 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
|
|||||||
Hook function to be executed after StockItem object is saved/updated
|
Hook function to be executed after StockItem object is saved/updated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Run this check in the background
|
if not InvenTree.ready.isImportingData():
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
# Run this check in the background
|
||||||
|
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
||||||
|
|
||||||
|
|
||||||
class StockItemAttachment(InvenTreeAttachment):
|
class StockItemAttachment(InvenTreeAttachment):
|
||||||
@ -2170,7 +2173,7 @@ class StockItemTestResult(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self):
|
def key(self):
|
||||||
return helpers.generateTestKey(self.test)
|
return InvenTree.helpers.generateTestKey(self.test)
|
||||||
|
|
||||||
stock_item = models.ForeignKey(
|
stock_item = models.ForeignKey(
|
||||||
StockItem,
|
StockItem,
|
||||||
|
@ -1056,6 +1056,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
|||||||
'{% url "api-stock-test-result-list" %}',
|
'{% url "api-stock-test-result-list" %}',
|
||||||
{
|
{
|
||||||
build: build_info.pk,
|
build: build_info.pk,
|
||||||
|
ordering: '-date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
success: function(results) {
|
success: function(results) {
|
||||||
|
@ -95,6 +95,9 @@ RUN echo "Downloading InvenTree from ${INVENTREE_GIT_REPO}"
|
|||||||
|
|
||||||
RUN git clone --branch ${INVENTREE_GIT_BRANCH} --depth 1 ${INVENTREE_GIT_REPO} ${INVENTREE_HOME}
|
RUN git clone --branch ${INVENTREE_GIT_BRANCH} --depth 1 ${INVENTREE_GIT_REPO} ${INVENTREE_HOME}
|
||||||
|
|
||||||
|
# Ref: https://github.blog/2022-04-12-git-security-vulnerability-announced/
|
||||||
|
RUN git config --global --add safe.directory ${INVENTREE_HOME}
|
||||||
|
|
||||||
# Checkout against a particular git tag
|
# Checkout against a particular git tag
|
||||||
RUN if [ -n "${INVENTREE_GIT_TAG}" ] ; then cd ${INVENTREE_HOME} && git fetch --all --tags && git checkout tags/${INVENTREE_GIT_TAG} -b v${INVENTREE_GIT_TAG}-branch ; fi
|
RUN if [ -n "${INVENTREE_GIT_TAG}" ] ; then cd ${INVENTREE_HOME} && git fetch --all --tags && git checkout tags/${INVENTREE_GIT_TAG} -b v${INVENTREE_GIT_TAG}-branch ; fi
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user