mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 77
|
||||
INVENTREE_API_VERSION = 78
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v78 -> 2022-10-25 : https://github.com/inventree/InvenTree/pull/3854
|
||||
- Make PartCategory to be filtered by name and description
|
||||
|
||||
v77 -> 2022-10-12 : https://github.com/inventree/InvenTree/pull/3772
|
||||
- Adds model permission checks for barcode assignment actions
|
||||
|
||||
|
@ -6,6 +6,7 @@ from urllib.parse import urlencode
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.sites.models import Site
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -227,7 +228,16 @@ class RegistratonMixin:
|
||||
return user
|
||||
|
||||
|
||||
class CustomAccountAdapter(RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
|
||||
class CustomUrlMixin:
|
||||
"""Mixin to set urls."""
|
||||
|
||||
def get_email_confirmation_url(self, request, emailconfirmation):
|
||||
"""Custom email confirmation (activation) url."""
|
||||
url = reverse("account_confirm_email", args=[emailconfirmation.key])
|
||||
return Site.objects.get_current().domain + url
|
||||
|
||||
|
||||
class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
|
||||
"""Override of adapter to use dynamic settings."""
|
||||
def send_mail(self, template_prefix, email, context):
|
||||
"""Only send mail if backend configured."""
|
||||
@ -236,7 +246,7 @@ class CustomAccountAdapter(RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
|
||||
return False
|
||||
|
||||
|
||||
class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
|
||||
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
|
||||
"""Override of adapter to use dynamic settings."""
|
||||
|
||||
def is_auto_signup_allowed(self, request, sociallogin):
|
||||
|
@ -629,6 +629,13 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
|
||||
def add_serial(serial):
|
||||
"""Helper function to check for duplicated values"""
|
||||
|
||||
serial = serial.strip()
|
||||
|
||||
# Ignore blank / emtpy serials
|
||||
if len(serial) == 0:
|
||||
return
|
||||
|
||||
if serial in serials:
|
||||
add_error(_("Duplicate serial") + f": {serial}")
|
||||
else:
|
||||
@ -645,6 +652,10 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
return serials
|
||||
|
||||
for group in groups:
|
||||
|
||||
# Calculate the "remaining" quantity of serial numbers
|
||||
remaining = expected_quantity - len(serials)
|
||||
|
||||
group = group.strip()
|
||||
|
||||
if '-' in group:
|
||||
@ -680,20 +691,21 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
group_items.append(b)
|
||||
break
|
||||
|
||||
elif count > expected_quantity:
|
||||
elif count > remaining:
|
||||
# More than the allowed number of items
|
||||
break
|
||||
|
||||
elif a_next is None:
|
||||
break
|
||||
|
||||
if len(group_items) > 0 and group_items[0] == a and group_items[-1] == b:
|
||||
if len(group_items) > remaining:
|
||||
add_error(_("Group range {g} exceeds allowed quantity ({q})".format(g=group, q=expected_quantity)))
|
||||
elif len(group_items) > 0 and group_items[0] == a and group_items[-1] == b:
|
||||
# In this case, the range extraction looks like it has worked
|
||||
for item in group_items:
|
||||
add_serial(item)
|
||||
else:
|
||||
add_serial(group)
|
||||
# add_error(_("Invalid group range: {g}").format(g=group))
|
||||
add_error(_("Invalid group range: {g}").format(g=group))
|
||||
|
||||
else:
|
||||
# In the case of a different number of hyphens, simply add the entire group
|
||||
@ -715,7 +727,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
continue
|
||||
elif len(items) == 2:
|
||||
try:
|
||||
if items[1] not in ['', None]:
|
||||
if items[1]:
|
||||
sequence_count = int(items[1]) + 1
|
||||
except ValueError:
|
||||
add_error(_("Invalid group sequence: {g}").format(g=group))
|
||||
@ -745,7 +757,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
if len(serials) == 0:
|
||||
raise ValidationError([_("No serial numbers found")])
|
||||
|
||||
if len(serials) != expected_quantity:
|
||||
if len(errors) == 0 and len(serials) != expected_quantity:
|
||||
raise ValidationError([_("Number of unique serial numbers ({s}) must match quantity ({q})").format(s=len(serials), q=expected_quantity)])
|
||||
|
||||
return serials
|
||||
|
@ -258,7 +258,7 @@ class TestHelpers(TestCase):
|
||||
def test_download_image(self):
|
||||
"""Test function for downloading image from remote URL"""
|
||||
|
||||
# Run check with a sequency of bad URLs
|
||||
# Run check with a sequence of bad URLs
|
||||
for url in [
|
||||
"blog",
|
||||
"htp://test.com/?",
|
||||
@ -268,17 +268,35 @@ class TestHelpers(TestCase):
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
helpers.download_image_from_url(url)
|
||||
|
||||
def dl_helper(url, expected_error, timeout=2.5, retries=3):
|
||||
"""Helper function for unit testing downloads.
|
||||
|
||||
As the httpstat.us service occassionaly refuses a connection,
|
||||
we will simply try multiple times
|
||||
"""
|
||||
|
||||
with self.assertRaises(expected_error):
|
||||
while retries > 0:
|
||||
|
||||
try:
|
||||
helpers.download_image_from_url(url, timeout=timeout)
|
||||
break
|
||||
except Exception as exc:
|
||||
if type(exc) is expected_error:
|
||||
# Re-throw this error
|
||||
raise exc
|
||||
|
||||
time.sleep(30)
|
||||
retries -= 1
|
||||
|
||||
# Attempt to download an image which throws a 404
|
||||
with self.assertRaises(requests.exceptions.HTTPError):
|
||||
helpers.download_image_from_url("https://httpstat.us/404", timeout=10)
|
||||
dl_helper("https://httpstat.us/404", requests.exceptions.HTTPError, timeout=10)
|
||||
|
||||
# Attempt to download, but timeout
|
||||
with self.assertRaises(requests.exceptions.Timeout):
|
||||
helpers.download_image_from_url("https://httpstat.us/200?sleep=5000")
|
||||
dl_helper("https://httpstat.us/200?sleep=5000", requests.exceptions.ReadTimeout, timeout=1)
|
||||
|
||||
# Attempt to download, but not a valid image
|
||||
with self.assertRaises(TypeError):
|
||||
helpers.download_image_from_url("https://httpstat.us/200", timeout=10)
|
||||
dl_helper("https://httpstat.us/200", TypeError, timeout=10)
|
||||
|
||||
large_img = "https://github.com/inventree/InvenTree/raw/master/InvenTree/InvenTree/static/img/paper_splash_large.jpg"
|
||||
|
||||
@ -429,11 +447,15 @@ class TestSerialNumberExtraction(TestCase):
|
||||
"""Test simple serial numbers."""
|
||||
e = helpers.extract_serial_numbers
|
||||
|
||||
# Test a range of numbers
|
||||
sn = e("1-5", 5, 1)
|
||||
self.assertEqual(len(sn), 5, 1)
|
||||
self.assertEqual(len(sn), 5)
|
||||
for i in range(1, 6):
|
||||
self.assertIn(str(i), sn)
|
||||
|
||||
sn = e("11-30", 20, 1)
|
||||
self.assertEqual(len(sn), 20)
|
||||
|
||||
sn = e("1, 2, 3, 4, 5", 5, 1)
|
||||
self.assertEqual(len(sn), 5)
|
||||
|
||||
@ -452,11 +474,6 @@ class TestSerialNumberExtraction(TestCase):
|
||||
self.assertEqual(len(sn), 5)
|
||||
self.assertEqual(sn, ['1', '2', "TG-4SR-92", '4', '5'])
|
||||
|
||||
# Test groups are not interpolated with alpha characters
|
||||
sn = e("1, A-2, 3+", 5, 1)
|
||||
self.assertEqual(len(sn), 5)
|
||||
self.assertEqual(sn, ['1', "A-2", '3', '4', '5'])
|
||||
|
||||
# Test multiple placeholders
|
||||
sn = e("1 2 ~ ~ ~", 5, 2)
|
||||
self.assertEqual(len(sn), 5)
|
||||
@ -521,6 +538,16 @@ class TestSerialNumberExtraction(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
e("1, 2, 3, E-5", 5, 1)
|
||||
|
||||
# Extract a range of values with a smaller range
|
||||
with self.assertRaises(ValidationError) as exc:
|
||||
e("11-50", 10, 1)
|
||||
self.assertIn('Range quantity exceeds 10', str(exc))
|
||||
|
||||
# Test groups are not interpolated with alpha characters
|
||||
with self.assertRaises(ValidationError) as exc:
|
||||
e("1, A-2, 3+", 5, 1)
|
||||
self.assertIn('Invalid group range: A-2', str(exc))
|
||||
|
||||
def test_combinations(self):
|
||||
"""Test complex serial number combinations."""
|
||||
e = helpers.extract_serial_numbers
|
||||
@ -541,6 +568,24 @@ class TestSerialNumberExtraction(TestCase):
|
||||
self.assertEqual(len(sn), 2)
|
||||
self.assertEqual(sn, ['14', '15'])
|
||||
|
||||
# Test multiple increment groups
|
||||
sn = e("~+4, 20+4, 30+4", 15, 10)
|
||||
self.assertEqual(len(sn), 15)
|
||||
|
||||
for v in [14, 24, 34]:
|
||||
self.assertIn(str(v), sn)
|
||||
|
||||
# Test multiple range groups
|
||||
sn = e("11-20, 41-50, 91-100", 30, 1)
|
||||
self.assertEqual(len(sn), 30)
|
||||
|
||||
for v in range(11, 21):
|
||||
self.assertIn(str(v), sn)
|
||||
for v in range(41, 51):
|
||||
self.assertIn(str(v), sn)
|
||||
for v in range(91, 101):
|
||||
self.assertIn(str(v), sn)
|
||||
|
||||
|
||||
class TestVersionNumber(TestCase):
|
||||
"""Unit tests for version number functions."""
|
||||
|
@ -8,6 +8,7 @@ from django.core import validators
|
||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from jinja2 import Template
|
||||
from moneyed import CURRENCIES
|
||||
|
||||
import common.models
|
||||
@ -158,14 +159,19 @@ def validate_overage(value):
|
||||
)
|
||||
|
||||
|
||||
def validate_part_name_format(self):
|
||||
def validate_part_name_format(value):
|
||||
"""Validate part name format.
|
||||
|
||||
Make sure that each template container has a field of Part Model
|
||||
"""
|
||||
|
||||
# Make sure that the field_name exists in Part model
|
||||
from part.models import Part
|
||||
|
||||
jinja_template_regex = re.compile('{{.*?}}')
|
||||
field_name_regex = re.compile('(?<=part\\.)[A-z]+')
|
||||
for jinja_template in jinja_template_regex.findall(str(self)):
|
||||
|
||||
for jinja_template in jinja_template_regex.findall(str(value)):
|
||||
# make sure at least one and only one field is present inside the parser
|
||||
field_names = field_name_regex.findall(jinja_template)
|
||||
if len(field_names) < 1:
|
||||
@ -173,9 +179,6 @@ def validate_part_name_format(self):
|
||||
'value': 'At least one field must be present inside a jinja template container i.e {{}}'
|
||||
})
|
||||
|
||||
# Make sure that the field_name exists in Part model
|
||||
from part.models import Part
|
||||
|
||||
for field_name in field_names:
|
||||
try:
|
||||
Part._meta.get_field(field_name)
|
||||
@ -184,4 +187,14 @@ def validate_part_name_format(self):
|
||||
'value': f'{field_name} does not exist in Part Model'
|
||||
})
|
||||
|
||||
# Attempt to render the template with a dummy Part instance
|
||||
p = Part(name='test part', description='some test part')
|
||||
|
||||
try:
|
||||
Template(value).render({'part': p})
|
||||
except Exception as exc:
|
||||
raise ValidationError({
|
||||
'value': str(exc)
|
||||
})
|
||||
|
||||
return True
|
||||
|
@ -1612,6 +1612,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'SEARCH_PREVIEW_SHOW_BUILD_ORDERS': {
|
||||
'name': _('Search Build Orders'),
|
||||
'description': _('Display build orders in search preview window'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS': {
|
||||
'name': _('Search Purchase Orders'),
|
||||
'description': _('Display purchase orders in search preview window'),
|
||||
|
@ -165,8 +165,10 @@ login_attempts: 5
|
||||
# Remote / proxy login
|
||||
# These settings can introduce security problems if configured incorrectly. Please read
|
||||
# https://docs.djangoproject.com/en/4.0/howto/auth-remote-user/ for more details
|
||||
# The header name should be prefixed by `HTTP`. Please read the docs for more details
|
||||
# https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest.META
|
||||
remote_login_enabled: False
|
||||
remote_login_header: REMOTE_USER
|
||||
remote_login_header: HTTP_REMOTE_USER
|
||||
|
||||
# Permit custom authentication backends
|
||||
#authentication_backends:
|
||||
|
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
File diff suppressed because it is too large
Load Diff
@ -153,6 +153,8 @@ class CategoryList(ListCreateAPI):
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
'name',
|
||||
'description'
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
|
@ -671,7 +671,7 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
|
||||
|
||||
return full_name
|
||||
|
||||
except AttributeError as attr_err:
|
||||
except Exception as attr_err:
|
||||
|
||||
logger.warning(f"exception while trying to create full name for part {self.name}", attr_err)
|
||||
|
||||
|
@ -48,8 +48,10 @@ class BarcodeScan(APIView):
|
||||
"""
|
||||
data = request.data
|
||||
|
||||
if 'barcode' not in data:
|
||||
raise ValidationError({'barcode': _('Must provide barcode_data parameter')})
|
||||
barcode_data = data.get('barcode', None)
|
||||
|
||||
if not barcode_data:
|
||||
raise ValidationError({'barcode': _('Missing barcode data')})
|
||||
|
||||
# Ensure that the default barcode handlers are run first
|
||||
plugins = [
|
||||
@ -57,7 +59,6 @@ class BarcodeScan(APIView):
|
||||
InvenTreeExternalBarcodePlugin(),
|
||||
] + registry.with_mixin('barcode')
|
||||
|
||||
barcode_data = data.get('barcode')
|
||||
barcode_hash = hash_barcode(barcode_data)
|
||||
|
||||
# Look for a barcode plugin which knows how to deal with this barcode
|
||||
@ -106,10 +107,10 @@ class BarcodeAssign(APIView):
|
||||
|
||||
data = request.data
|
||||
|
||||
if 'barcode' not in data:
|
||||
raise ValidationError({'barcode': _('Must provide barcode_data parameter')})
|
||||
barcode_data = data.get('barcode', None)
|
||||
|
||||
barcode_data = data['barcode']
|
||||
if not barcode_data:
|
||||
raise ValidationError({'barcode': _('Missing barcode data')})
|
||||
|
||||
# Here we only check against 'InvenTree' plugins
|
||||
plugins = [
|
||||
|
@ -55,7 +55,8 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
data = response.data
|
||||
self.assertIn('error', data)
|
||||
self.assertIn('barcode', data)
|
||||
self.assertIn('Missing barcode data', str(response.data['barcode']))
|
||||
|
||||
def test_find_part(self):
|
||||
"""Test that we can lookup a part based on ID."""
|
||||
|
@ -23,6 +23,7 @@
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK" user_setting=True icon='fa-eye-slash' %}
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_LOCATIONS" user_setting=True icon='fa-sitemap' %}
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_COMPANIES" user_setting=True icon='fa-building' %}
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_BUILD_ORDERS" user_setting=True icon='fa-tools' %}
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS" user_setting=True icon='fa-shopping-cart' %}
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_EXCLUDE_INACTIVE_PURCHASE_ORDERS" user_setting=True icon='fa-eye-slash' %}
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_SALES_ORDERS" user_setting=True icon='fa-truck' %}
|
||||
|
@ -45,7 +45,7 @@
|
||||
<tr>
|
||||
<td><span class='fas fa-code'></span></td>
|
||||
<td>{% trans "API Version" %}</td>
|
||||
<td>{% inventree_api_version %}{% include "clip.html" %}</td>
|
||||
<td><a href="/api-doc/">{% inventree_api_version %}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
|
@ -1134,7 +1134,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
data.push(row);
|
||||
});
|
||||
|
||||
$(table).bootstrapTable('load', row);
|
||||
$(table).bootstrapTable('load', data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -205,9 +205,6 @@ function constructChangeForm(fields, options) {
|
||||
},
|
||||
success: function(data) {
|
||||
|
||||
// Ensure the data are fully sanitized before we operate on it
|
||||
data = sanitizeData(data);
|
||||
|
||||
// An optional function can be provided to process the returned results,
|
||||
// before they are rendered to the form
|
||||
if (options.processResults) {
|
||||
|
@ -185,14 +185,14 @@ function makeProgressBar(value, maximum, opts={}) {
|
||||
|
||||
var options = opts || {};
|
||||
|
||||
value = parseFloat(value);
|
||||
value = formatDecimal(parseFloat(value));
|
||||
|
||||
var percent = 100;
|
||||
|
||||
// Prevent div-by-zero or null value
|
||||
if (maximum && maximum > 0) {
|
||||
maximum = parseFloat(maximum);
|
||||
percent = parseInt(value / maximum * 100);
|
||||
maximum = formatDecimal(parseFloat(maximum));
|
||||
percent = formatDecimal(parseInt(value / maximum * 100));
|
||||
}
|
||||
|
||||
if (percent > 100) {
|
||||
|
@ -247,6 +247,22 @@ function updateSearch() {
|
||||
);
|
||||
}
|
||||
|
||||
if (checkPermission('build') && user_settings.SEARCH_PREVIEW_SHOW_BUILD_ORDERS) {
|
||||
// Search for matching build orders
|
||||
addSearchQuery(
|
||||
'build',
|
||||
'{% trans "Build Orders" %}',
|
||||
'{% url "api-build-list" %}',
|
||||
{
|
||||
part_detail: true,
|
||||
},
|
||||
renderBuild,
|
||||
{
|
||||
url: '/build',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ((checkPermission('sales_order') || checkPermission('purchase_order')) && user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) {
|
||||
// Search for matching companies
|
||||
addSearchQuery(
|
||||
|
@ -104,6 +104,7 @@ InvenTree is designed to be **extensible**, and provides multiple options for **
|
||||
<li><a href="https://hub.docker.com/r/inventree/inventree">Docker</a></li>
|
||||
<li><a href="https://crowdin.com/project/inventree">Crowdin</a></li>
|
||||
<li><a href="https://coveralls.io/github/inventree/InvenTree">Coveralls</a></li>
|
||||
<li><a href="https://packager.io/gh/inventree/InvenTree">Packager.io</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
@ -136,7 +137,7 @@ There are several options to deploy InvenTree.
|
||||
<a href="https://inventree.readthedocs.io/en/latest/start/install/">Bare Metal</a>
|
||||
</h4></div>
|
||||
|
||||
Single line install:
|
||||
Single line install - read [the docs](https://inventree.readthedocs.io/en/latest/start/installer/) for supported distros and details about the function:
|
||||
```bash
|
||||
curl https://raw.githubusercontent.com/InvenTree/InvenTree/master/contrib/install.sh | sh
|
||||
```
|
||||
|
Reference in New Issue
Block a user