mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-21 14:20:54 +00:00
Merge branch 'master' of github.com:inventree/InvenTree into part_main_details
This commit is contained in:
InvenTree
InvenTree
fields.pyhelpers.pymetadata.pymodels.pytest_urls.pyurls.py
static
css
script
jquery-ui
images
ui-icons_444444_256x240.pngui-icons_555555_256x240.pngui-icons_777620_256x240.pngui-icons_777777_256x240.pngui-icons_cc0000_256x240.pngui-icons_ffffff_256x240.png
index.htmljquery-ui.cssjquery-ui.jsjquery-ui.min.cssjquery-ui.min.jsjquery-ui.structure.cssjquery-ui.structure.min.cssjquery-ui.theme.min.cssbuild
common
company
label
order
part
report
stock
templates
users
@ -20,7 +20,6 @@ from djmoney.forms.fields import MoneyField
|
||||
from djmoney.models.validators import MinMoneyValidator
|
||||
|
||||
import InvenTree.helpers
|
||||
import common.settings
|
||||
|
||||
|
||||
class InvenTreeURLFormField(FormURLField):
|
||||
@ -42,9 +41,11 @@ class InvenTreeURLField(models.URLField):
|
||||
|
||||
def money_kwargs():
|
||||
""" returns the database settings for MoneyFields """
|
||||
from common.settings import currency_code_mappings, currency_code_default
|
||||
|
||||
kwargs = {}
|
||||
kwargs['currency_choices'] = common.settings.currency_code_mappings()
|
||||
kwargs['default_currency'] = common.settings.currency_code_default
|
||||
kwargs['currency_choices'] = currency_code_mappings()
|
||||
kwargs['default_currency'] = currency_code_default()
|
||||
return kwargs
|
||||
|
||||
|
||||
@ -55,7 +56,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# detect if creating migration
|
||||
if 'makemigrations' in sys.argv:
|
||||
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
|
||||
# remove currency information for a clean migration
|
||||
kwargs['default_currency'] = ''
|
||||
kwargs['currency_choices'] = []
|
||||
|
@ -631,13 +631,34 @@ def clean_decimal(number):
|
||||
""" Clean-up decimal value """
|
||||
|
||||
# Check if empty
|
||||
if number is None or number == '':
|
||||
if number is None or number == '' or number == 0:
|
||||
return Decimal(0)
|
||||
|
||||
# Check if decimal type
|
||||
# Convert to string and remove spaces
|
||||
number = str(number).replace(' ', '')
|
||||
|
||||
# Guess what type of decimal and thousands separators are used
|
||||
count_comma = number.count(',')
|
||||
count_point = number.count('.')
|
||||
|
||||
if count_comma == 1:
|
||||
# Comma is used as decimal separator
|
||||
if count_point > 0:
|
||||
# Points are used as thousands separators: remove them
|
||||
number = number.replace('.', '')
|
||||
# Replace decimal separator with point
|
||||
number = number.replace(',', '.')
|
||||
elif count_point == 1:
|
||||
# Point is used as decimal separator
|
||||
if count_comma > 0:
|
||||
# Commas are used as thousands separators: remove them
|
||||
number = number.replace(',', '')
|
||||
|
||||
# Convert to Decimal type
|
||||
try:
|
||||
clean_number = Decimal(number)
|
||||
except InvalidOperation:
|
||||
clean_number = number
|
||||
# Number cannot be converted to Decimal (eg. a string containing letters)
|
||||
return Decimal(0)
|
||||
|
||||
return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()
|
||||
|
@ -32,6 +32,9 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
|
||||
self.request = request
|
||||
self.view = view
|
||||
|
||||
metadata = super().determine_metadata(request, view)
|
||||
|
||||
user = request.user
|
||||
@ -136,6 +139,42 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Try to extract 'instance' information
|
||||
instance = None
|
||||
|
||||
# Extract extra information if an instance is available
|
||||
if hasattr(serializer, 'instance'):
|
||||
instance = serializer.instance
|
||||
|
||||
if instance is None:
|
||||
try:
|
||||
instance = self.view.get_object()
|
||||
except:
|
||||
pass
|
||||
|
||||
if instance is not None:
|
||||
"""
|
||||
If there is an instance associated with this API View,
|
||||
introspect that instance to find any specific API info.
|
||||
"""
|
||||
|
||||
if hasattr(instance, 'api_instance_filters'):
|
||||
|
||||
instance_filters = instance.api_instance_filters()
|
||||
|
||||
for field_name, field_filters in instance_filters.items():
|
||||
|
||||
if field_name not in serializer_info.keys():
|
||||
# The field might be missing, but is added later on
|
||||
# This function seems to get called multiple times?
|
||||
continue
|
||||
|
||||
if 'instance_filters' not in serializer_info[field_name].keys():
|
||||
serializer_info[field_name]['instance_filters'] = {}
|
||||
|
||||
for key, value in field_filters.items():
|
||||
serializer_info[field_name]['instance_filters'][key] = value
|
||||
|
||||
return serializer_info
|
||||
|
||||
def get_field_info(self, field):
|
||||
|
@ -93,6 +93,17 @@ class InvenTreeTree(MPTTModel):
|
||||
parent: The item immediately above this one. An item with a null parent is a top-level item
|
||||
"""
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""
|
||||
Instance filters for InvenTreeTree models
|
||||
"""
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
|
@ -1037,3 +1037,10 @@ a.anchor {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.search-menu {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.search-menu .ui-menu-item {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
Binary file not shown.
Before ![]() (image error) Size: 6.9 KiB After ![]() (image error) Size: 6.9 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 6.9 KiB After ![]() (image error) Size: 6.9 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 4.5 KiB After ![]() (image error) Size: 4.5 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 6.9 KiB After ![]() (image error) Size: 6.9 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 4.5 KiB After ![]() (image error) Size: 4.5 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 6.3 KiB After ![]() (image error) Size: 6.3 KiB ![]() ![]() |
@ -59,6 +59,11 @@
|
||||
<h1>YOUR COMPONENTS:</h1>
|
||||
|
||||
|
||||
<!-- Autocomplete -->
|
||||
<h2 class="demoHeaders">Autocomplete</h2>
|
||||
<div>
|
||||
<input id="autocomplete" title="type "a"">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -248,6 +253,23 @@
|
||||
|
||||
|
||||
|
||||
<!-- Menu -->
|
||||
<h2 class="demoHeaders">Menu</h2>
|
||||
<ul style="width:100px;" id="menu">
|
||||
<li><div>Item 1</div></li>
|
||||
<li><div>Item 2</div></li>
|
||||
<li><div>Item 3</div>
|
||||
<ul>
|
||||
<li><div>Item 3-1</div></li>
|
||||
<li><div>Item 3-2</div></li>
|
||||
<li><div>Item 3-3</div></li>
|
||||
<li><div>Item 3-4</div></li>
|
||||
<li><div>Item 3-5</div></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><div>Item 4</div></li>
|
||||
<li><div>Item 5</div></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- Highlight / Error -->
|
||||
@ -270,6 +292,33 @@
|
||||
<script src="jquery-ui.js"></script>
|
||||
<script>
|
||||
|
||||
var availableTags = [
|
||||
"ActionScript",
|
||||
"AppleScript",
|
||||
"Asp",
|
||||
"BASIC",
|
||||
"C",
|
||||
"C++",
|
||||
"Clojure",
|
||||
"COBOL",
|
||||
"ColdFusion",
|
||||
"Erlang",
|
||||
"Fortran",
|
||||
"Groovy",
|
||||
"Haskell",
|
||||
"Java",
|
||||
"JavaScript",
|
||||
"Lisp",
|
||||
"Perl",
|
||||
"PHP",
|
||||
"Python",
|
||||
"Ruby",
|
||||
"Scala",
|
||||
"Scheme"
|
||||
];
|
||||
$( "#autocomplete" ).autocomplete({
|
||||
source: availableTags
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -280,6 +329,7 @@
|
||||
|
||||
|
||||
|
||||
$( "#menu" ).menu();
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Includes: core.css, resizable.css, theme.css
|
||||
* Includes: core.css, resizable.css, autocomplete.css, menu.css, theme.css
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
@ -160,6 +160,66 @@
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ui-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* support: IE10, see #8844 */
|
||||
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||
}
|
||||
.ui-menu .ui-menu-item-wrapper {
|
||||
position: relative;
|
||||
padding: 3px 1em 3px .4em;
|
||||
}
|
||||
.ui-menu .ui-menu-divider {
|
||||
margin: 5px 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.ui-menu .ui-state-focus,
|
||||
.ui-menu .ui-state-active {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
/* icon support */
|
||||
.ui-menu-icons {
|
||||
position: relative;
|
||||
}
|
||||
.ui-menu-icons .ui-menu-item-wrapper {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* left-aligned */
|
||||
.ui-menu .ui-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: .2em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* right-aligned */
|
||||
.ui-menu .ui-menu-icon {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Component containers
|
||||
----------------------------------*/
|
||||
|
1915
InvenTree/InvenTree/static/script/jquery-ui/jquery-ui.js
vendored
1915
InvenTree/InvenTree/static/script/jquery-ui/jquery-ui.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -164,3 +164,63 @@
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ui-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* support: IE10, see #8844 */
|
||||
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||
}
|
||||
.ui-menu .ui-menu-item-wrapper {
|
||||
position: relative;
|
||||
padding: 3px 1em 3px .4em;
|
||||
}
|
||||
.ui-menu .ui-menu-divider {
|
||||
margin: 5px 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.ui-menu .ui-state-focus,
|
||||
.ui-menu .ui-state-active {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
/* icon support */
|
||||
.ui-menu-icons {
|
||||
position: relative;
|
||||
}
|
||||
.ui-menu-icons .ui-menu-item-wrapper {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* left-aligned */
|
||||
.ui-menu .ui-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: .2em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* right-aligned */
|
||||
.ui-menu .ui-menu-icon {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}
|
||||
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}
|
@ -1,4 +1,4 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
|
142
InvenTree/InvenTree/test_urls.py
Normal file
142
InvenTree/InvenTree/test_urls.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""
|
||||
Validate that all URLs specified in template files are correct.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class URLTest(TestCase):
|
||||
|
||||
# Need fixture data in the database
|
||||
fixtures = [
|
||||
'settings',
|
||||
'build',
|
||||
'company',
|
||||
'manufacturer_part',
|
||||
'price_breaks',
|
||||
'supplier_part',
|
||||
'order',
|
||||
'sales_order',
|
||||
'bom',
|
||||
'category',
|
||||
'params',
|
||||
'part_pricebreaks',
|
||||
'part',
|
||||
'test_templates',
|
||||
'location',
|
||||
'stock_tests',
|
||||
'stock',
|
||||
'users',
|
||||
]
|
||||
|
||||
def find_files(self, suffix):
|
||||
"""
|
||||
Search for all files in the template directories,
|
||||
which can have URLs rendered
|
||||
"""
|
||||
|
||||
template_dirs = [
|
||||
('build', 'templates'),
|
||||
('common', 'templates'),
|
||||
('company', 'templates'),
|
||||
('label', 'templates'),
|
||||
('order', 'templates'),
|
||||
('part', 'templates'),
|
||||
('report', 'templates'),
|
||||
('stock', 'templates'),
|
||||
('templates', ),
|
||||
]
|
||||
|
||||
template_files = []
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
tld = os.path.join(here, '..')
|
||||
|
||||
for directory in template_dirs:
|
||||
|
||||
template_dir = os.path.join(tld, *directory)
|
||||
|
||||
for path in Path(template_dir).rglob(suffix):
|
||||
|
||||
f = os.path.abspath(path)
|
||||
|
||||
if f not in template_files:
|
||||
template_files.append(f)
|
||||
|
||||
return template_files
|
||||
|
||||
def find_urls(self, input_file):
|
||||
"""
|
||||
Search for all instances of {% url %} in supplied template file
|
||||
"""
|
||||
|
||||
urls = []
|
||||
|
||||
pattern = "{% url ['\"]([^'\"]+)['\"]([^%]*)%}"
|
||||
|
||||
with open(input_file, 'r') as f:
|
||||
|
||||
data = f.read()
|
||||
|
||||
results = re.findall(pattern, data)
|
||||
|
||||
for result in results:
|
||||
if len(result) == 2:
|
||||
urls.append([
|
||||
result[0].strip(),
|
||||
result[1].strip()
|
||||
])
|
||||
elif len(result) == 1:
|
||||
urls.append([
|
||||
result[0].strip(),
|
||||
''
|
||||
])
|
||||
|
||||
return urls
|
||||
|
||||
def reverse_url(self, url_pair):
|
||||
"""
|
||||
Perform lookup on the URL
|
||||
"""
|
||||
|
||||
url, pk = url_pair
|
||||
|
||||
# TODO: Handle reverse lookup of admin URLs!
|
||||
if url.startswith("admin:"):
|
||||
return
|
||||
|
||||
if pk:
|
||||
# We will assume that there is at least one item in the database
|
||||
reverse(url, kwargs={"pk": 1})
|
||||
else:
|
||||
reverse(url)
|
||||
|
||||
def check_file(self, f):
|
||||
"""
|
||||
Run URL checks for the provided file
|
||||
"""
|
||||
|
||||
urls = self.find_urls(f)
|
||||
|
||||
for url in urls:
|
||||
self.reverse_url(url)
|
||||
|
||||
def test_html_templates(self):
|
||||
|
||||
template_files = self.find_files("*.html")
|
||||
|
||||
for f in template_files:
|
||||
self.check_file(f)
|
||||
|
||||
def test_js_templates(self):
|
||||
|
||||
template_files = self.find_files("*.js")
|
||||
|
||||
for f in template_files:
|
||||
self.check_file(f)
|
@ -43,7 +43,7 @@ from .views import CurrencySettingsView, CurrencyRefreshView
|
||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||
from .views import DynamicJsView
|
||||
|
||||
from common.views import SettingEdit
|
||||
from common.views import SettingEdit, UserSettingEdit
|
||||
|
||||
from .api import InfoView, NotFoundView
|
||||
from .api import ActionPluginView
|
||||
@ -79,6 +79,7 @@ apipatterns = [
|
||||
|
||||
settings_urls = [
|
||||
|
||||
url(r'^usersettings/', SettingsView.as_view(template_name='InvenTree/settings/user_settings.html'), name='settings-user-settings'),
|
||||
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||
@ -94,6 +95,7 @@ settings_urls = [
|
||||
url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'),
|
||||
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'),
|
||||
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
||||
|
||||
# Catch any other urls
|
||||
@ -111,6 +113,7 @@ dynamic_javascript_urls = [
|
||||
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
|
||||
url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'),
|
||||
url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'),
|
||||
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/inventree.js'), name='inventree.js'),
|
||||
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
|
||||
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'),
|
||||
url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'),
|
||||
|
@ -104,6 +104,21 @@ class BuildList(generics.ListCreateAPIView):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# exclude parent tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
|
||||
try:
|
||||
build = Build.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[bld.pk for bld in build.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, Build.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by "parent"
|
||||
parent = params.get('parent', None)
|
||||
|
||||
|
@ -96,6 +96,14 @@ class Build(MPTTModel):
|
||||
def get_api_url():
|
||||
return reverse('api-build-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
|
@ -5,7 +5,7 @@ from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from .models import InvenTreeSetting
|
||||
from .models import InvenTreeSetting, InvenTreeUserSetting
|
||||
|
||||
|
||||
class SettingsAdmin(ImportExportModelAdmin):
|
||||
@ -13,4 +13,10 @@ class SettingsAdmin(ImportExportModelAdmin):
|
||||
list_display = ('key', 'value')
|
||||
|
||||
|
||||
class UserSettingsAdmin(ImportExportModelAdmin):
|
||||
|
||||
list_display = ('key', 'value', 'user', )
|
||||
|
||||
|
||||
admin.site.register(InvenTreeSetting, SettingsAdmin)
|
||||
admin.site.register(InvenTreeUserSetting, UserSettingsAdmin)
|
||||
|
@ -53,17 +53,20 @@ class FileManager:
|
||||
|
||||
ext = os.path.splitext(file.name)[-1].lower().replace('.', '')
|
||||
|
||||
if ext in ['csv', 'tsv', ]:
|
||||
# These file formats need string decoding
|
||||
raw_data = file.read().decode('utf-8')
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
|
||||
raw_data = file.read()
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
else:
|
||||
raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
|
||||
try:
|
||||
if ext in ['csv', 'tsv', ]:
|
||||
# These file formats need string decoding
|
||||
raw_data = file.read().decode('utf-8')
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
|
||||
raw_data = file.read()
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
else:
|
||||
raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
|
||||
except UnicodeEncodeError:
|
||||
raise ValidationError(_('Error reading file (invalid encoding)'))
|
||||
|
||||
try:
|
||||
cleaned_data = tablib.Dataset().load(raw_data, format=ext)
|
||||
|
33
InvenTree/common/migrations/0011_auto_20210722_2114.py
Normal file
33
InvenTree/common/migrations/0011_auto_20210722_2114.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-22 21:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('common', '0010_migrate_currency_setting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InvenTreeUserSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
|
||||
('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50)),
|
||||
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'InvenTree User Setting',
|
||||
'verbose_name_plural': 'InvenTree User Settings',
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='inventreeusersetting',
|
||||
constraint=models.UniqueConstraint(fields=('key', 'user'), name='unique key and user'),
|
||||
),
|
||||
]
|
@ -11,6 +11,7 @@ import decimal
|
||||
import math
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.utils import IntegrityError, OperationalError
|
||||
from django.conf import settings
|
||||
|
||||
@ -18,8 +19,6 @@ from djmoney.settings import CURRENCY_CHOICES
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
|
||||
import common.settings
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -28,7 +27,397 @@ import InvenTree.helpers
|
||||
import InvenTree.fields
|
||||
|
||||
|
||||
class InvenTreeSetting(models.Model):
|
||||
class BaseInvenTreeSetting(models.Model):
|
||||
"""
|
||||
An base InvenTreeSetting object is a key:value pair used for storing
|
||||
single values (e.g. one-off settings values).
|
||||
"""
|
||||
|
||||
GLOBAL_SETTINGS = {}
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def get_setting_name(cls, key):
|
||||
"""
|
||||
Return the name of a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('name', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_setting_description(cls, key):
|
||||
"""
|
||||
Return the description for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('description', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_setting_units(cls, key):
|
||||
"""
|
||||
Return the units for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('units', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_setting_validator(cls, key):
|
||||
"""
|
||||
Return the validator for a particular setting.
|
||||
|
||||
If it does not exist, return None
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('validator', None)
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_setting_default(cls, key):
|
||||
"""
|
||||
Return the default value for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('default', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_setting_choices(cls, key):
|
||||
"""
|
||||
Return the validator choices available for a particular setting.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
choices = setting.get('choices', None)
|
||||
else:
|
||||
choices = None
|
||||
|
||||
"""
|
||||
TODO:
|
||||
if type(choices) is function:
|
||||
# Evaluate the function (we expect it will return a list of tuples...)
|
||||
return choices()
|
||||
"""
|
||||
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls, key, **kwargs):
|
||||
return {'key__iexact': key}
|
||||
|
||||
@classmethod
|
||||
def get_setting_object(cls, key, **kwargs):
|
||||
"""
|
||||
Return an InvenTreeSetting object matching the given key.
|
||||
|
||||
- Key is case-insensitive
|
||||
- Returns None if no match is made
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
try:
|
||||
setting = cls.objects.filter(**cls.get_filters(key, **kwargs)).first()
|
||||
except (ValueError, cls.DoesNotExist):
|
||||
setting = None
|
||||
except (IntegrityError, OperationalError):
|
||||
setting = None
|
||||
|
||||
# Setting does not exist! (Try to create it)
|
||||
if not setting:
|
||||
|
||||
setting = cls(key=key, value=cls.get_setting_default(key), **kwargs)
|
||||
|
||||
try:
|
||||
# Wrap this statement in "atomic", so it can be rolled back if it fails
|
||||
with transaction.atomic():
|
||||
setting.save()
|
||||
except (IntegrityError, OperationalError):
|
||||
# It might be the case that the database isn't created yet
|
||||
pass
|
||||
|
||||
return setting
|
||||
|
||||
@classmethod
|
||||
def get_setting_pk(cls, key):
|
||||
"""
|
||||
Return the primary-key value for a given setting.
|
||||
|
||||
If the setting does not exist, return None
|
||||
"""
|
||||
|
||||
setting = cls.get_setting_object(cls)
|
||||
|
||||
if setting:
|
||||
return setting.pk
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_setting(cls, key, backup_value=None, **kwargs):
|
||||
"""
|
||||
Get the value of a particular setting.
|
||||
If it does not exist, return the backup value (default = None)
|
||||
"""
|
||||
|
||||
# If no backup value is specified, atttempt to retrieve a "default" value
|
||||
if backup_value is None:
|
||||
backup_value = cls.get_setting_default(key)
|
||||
|
||||
setting = cls.get_setting_object(key, **kwargs)
|
||||
|
||||
if setting:
|
||||
value = setting.value
|
||||
|
||||
# If the particular setting is defined as a boolean, cast the value to a boolean
|
||||
if setting.is_bool():
|
||||
value = InvenTree.helpers.str2bool(value)
|
||||
|
||||
if setting.is_int():
|
||||
try:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
value = backup_value
|
||||
|
||||
else:
|
||||
value = backup_value
|
||||
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def set_setting(cls, key, value, change_user, create=True, **kwargs):
|
||||
"""
|
||||
Set the value of a particular setting.
|
||||
If it does not exist, option to create it.
|
||||
|
||||
Args:
|
||||
key: settings key
|
||||
value: New value
|
||||
change_user: User object (must be staff member to update a core setting)
|
||||
create: If True, create a new setting if the specified key does not exist.
|
||||
"""
|
||||
|
||||
if change_user is not None and not change_user.is_staff:
|
||||
return
|
||||
|
||||
try:
|
||||
setting = cls.objects.get(**cls.get_filters(key, **kwargs))
|
||||
except cls.DoesNotExist:
|
||||
|
||||
if create:
|
||||
setting = cls(key=key, **kwargs)
|
||||
else:
|
||||
return
|
||||
|
||||
# Enforce standard boolean representation
|
||||
if setting.is_bool():
|
||||
value = InvenTree.helpers.str2bool(value)
|
||||
|
||||
setting.value = str(value)
|
||||
setting.save()
|
||||
|
||||
key = models.CharField(max_length=50, blank=False, unique=False, help_text=_('Settings key (must be unique - case insensitive'))
|
||||
|
||||
value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.get_setting_name(self.key)
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
return self.__class__.get_setting_default(self.key)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.__class__.get_setting_description(self.key)
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return self.__class__.get_setting_units(self.key)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
If a validator (or multiple validators) are defined for a particular setting key,
|
||||
run them against the 'value' field.
|
||||
"""
|
||||
|
||||
super().clean()
|
||||
|
||||
validator = self.__class__.get_setting_validator(self.key)
|
||||
|
||||
if self.is_bool():
|
||||
self.value = InvenTree.helpers.str2bool(self.value)
|
||||
|
||||
if self.is_int():
|
||||
try:
|
||||
self.value = int(self.value)
|
||||
except (ValueError):
|
||||
raise ValidationError(_('Must be an integer value'))
|
||||
|
||||
if validator is not None:
|
||||
self.run_validator(validator)
|
||||
|
||||
def run_validator(self, validator):
|
||||
"""
|
||||
Run a validator against the 'value' field for this InvenTreeSetting object.
|
||||
"""
|
||||
|
||||
if validator is None:
|
||||
return
|
||||
|
||||
value = self.value
|
||||
|
||||
# Boolean validator
|
||||
if self.is_bool():
|
||||
# Value must "look like" a boolean value
|
||||
if InvenTree.helpers.is_bool(value):
|
||||
# Coerce into either "True" or "False"
|
||||
value = InvenTree.helpers.str2bool(value)
|
||||
else:
|
||||
raise ValidationError({
|
||||
'value': _('Value must be a boolean value')
|
||||
})
|
||||
|
||||
# Integer validator
|
||||
if self.is_int():
|
||||
|
||||
try:
|
||||
# Coerce into an integer value
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError({
|
||||
'value': _('Value must be an integer value'),
|
||||
})
|
||||
|
||||
# If a list of validators is supplied, iterate through each one
|
||||
if type(validator) in [list, tuple]:
|
||||
for v in validator:
|
||||
self.run_validator(v)
|
||||
|
||||
if callable(validator):
|
||||
# We can accept function validators with a single argument
|
||||
validator(self.value)
|
||||
|
||||
def validate_unique(self, exclude=None, **kwargs):
|
||||
""" Ensure that the key:value pair is unique.
|
||||
In addition to the base validators, this ensures that the 'key'
|
||||
is unique, using a case-insensitive comparison.
|
||||
"""
|
||||
|
||||
super().validate_unique(exclude)
|
||||
|
||||
try:
|
||||
setting = self.__class__.objects.exclude(id=self.id).filter(**self.get_filters(self.key, **kwargs))
|
||||
if setting.exists():
|
||||
raise ValidationError({'key': _('Key string must be unique')})
|
||||
except self.DoesNotExist:
|
||||
pass
|
||||
|
||||
def choices(self):
|
||||
"""
|
||||
Return the available choices for this setting (or None if no choices are defined)
|
||||
"""
|
||||
|
||||
return self.__class__.get_setting_choices(self.key)
|
||||
|
||||
def is_bool(self):
|
||||
"""
|
||||
Check if this setting is required to be a boolean value
|
||||
"""
|
||||
|
||||
validator = self.__class__.get_setting_validator(self.key)
|
||||
|
||||
if validator == bool:
|
||||
return True
|
||||
|
||||
if type(validator) in [list, tuple]:
|
||||
for v in validator:
|
||||
if v == bool:
|
||||
return True
|
||||
|
||||
def as_bool(self):
|
||||
"""
|
||||
Return the value of this setting converted to a boolean value.
|
||||
|
||||
Warning: Only use on values where is_bool evaluates to true!
|
||||
"""
|
||||
|
||||
return InvenTree.helpers.str2bool(self.value)
|
||||
|
||||
def is_int(self):
|
||||
"""
|
||||
Check if the setting is required to be an integer value:
|
||||
"""
|
||||
|
||||
validator = self.__class__.get_setting_validator(self.key)
|
||||
|
||||
if validator == int:
|
||||
return True
|
||||
|
||||
if type(validator) in [list, tuple]:
|
||||
for v in validator:
|
||||
if v == int:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def as_int(self):
|
||||
"""
|
||||
Return the value of this setting converted to a boolean value.
|
||||
|
||||
If an error occurs, return the default value
|
||||
"""
|
||||
|
||||
try:
|
||||
value = int(self.value)
|
||||
except (ValueError, TypeError):
|
||||
value = self.default_value()
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
"""
|
||||
An InvenTreeSetting object is a key:value pair used for storing
|
||||
single values (e.g. one-off settings values).
|
||||
@ -279,6 +668,13 @@ class InvenTreeSetting(models.Model):
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'SEARCH_PREVIEW_RESULTS': {
|
||||
'name': _('Search Preview Results'),
|
||||
'description': _('Number of results to show in search preview window'),
|
||||
'default': 10,
|
||||
'validator': [int, MinValueValidator(1)]
|
||||
},
|
||||
|
||||
'STOCK_ENABLE_EXPIRY': {
|
||||
'name': _('Stock Expiry'),
|
||||
'description': _('Enable stock expiry functionality'),
|
||||
@ -357,379 +753,144 @@ class InvenTreeSetting(models.Model):
|
||||
verbose_name = "InvenTree Setting"
|
||||
verbose_name_plural = "InvenTree Settings"
|
||||
|
||||
@classmethod
|
||||
def get_setting_name(cls, key):
|
||||
"""
|
||||
Return the name of a particular setting.
|
||||
key = models.CharField(
|
||||
max_length=50,
|
||||
blank=False,
|
||||
unique=True,
|
||||
help_text=_('Settings key (must be unique - case insensitive'),
|
||||
)
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
"""
|
||||
An InvenTreeSetting object with a usercontext
|
||||
"""
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('name', '')
|
||||
else:
|
||||
return ''
|
||||
GLOBAL_SETTINGS = {
|
||||
'HOMEPAGE_PART_STARRED': {
|
||||
'name': _('Show starred parts'),
|
||||
'description': _('Show starred parts on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_PART_LATEST': {
|
||||
'name': _('Show latest parts'),
|
||||
'description': _('Show latest parts on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_BOM_VALIDATION': {
|
||||
'name': _('Show unvalidated BOMs'),
|
||||
'description': _('Show BOMs that await validation on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_STOCK_RECENT': {
|
||||
'name': _('Show recent stock changes'),
|
||||
'description': _('Show recently changed stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_STOCK_LOW': {
|
||||
'name': _('Show low stock'),
|
||||
'description': _('Show low stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_STOCK_DEPLETED': {
|
||||
'name': _('Show depleted stock'),
|
||||
'description': _('Show depleted stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_STOCK_NEEDED': {
|
||||
'name': _('Show needed stock'),
|
||||
'description': _('Show stock items needed for builds on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_STOCK_EXPIRED': {
|
||||
'name': _('Show expired stock'),
|
||||
'description': _('Show expired stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_STOCK_STALE': {
|
||||
'name': _('Show stale stock'),
|
||||
'description': _('Show stale stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_BUILD_PENDING': {
|
||||
'name': _('Show pending builds'),
|
||||
'description': _('Show pending builds on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_BUILD_OVERDUE': {
|
||||
'name': _('Show overdue builds'),
|
||||
'description': _('Show overdue builds on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_PO_OUTSTANDING': {
|
||||
'name': _('Show outstanding POs'),
|
||||
'description': _('Show outstanding POs on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_PO_OVERDUE': {
|
||||
'name': _('Show overdue POs'),
|
||||
'description': _('Show overdue POs on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_SO_OUTSTANDING': {
|
||||
'name': _('Show outstanding SOs'),
|
||||
'description': _('Show outstanding SOs on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'HOMEPAGE_SO_OVERDUE': {
|
||||
'name': _('Show overdue SOs'),
|
||||
'description': _('Show overdue SOs on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
}
|
||||
|
||||
class Meta:
|
||||
verbose_name = "InvenTree User Setting"
|
||||
verbose_name_plural = "InvenTree User Settings"
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['key', 'user'], name='unique key and user')
|
||||
]
|
||||
|
||||
key = models.CharField(
|
||||
max_length=50,
|
||||
blank=False,
|
||||
unique=False,
|
||||
help_text=_('Settings key (must be unique - case insensitive'),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True, null=True,
|
||||
verbose_name=_('User'),
|
||||
help_text=_('User'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_setting_description(cls, key):
|
||||
"""
|
||||
Return the description for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('description', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_setting_units(cls, key):
|
||||
"""
|
||||
Return the units for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('units', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_setting_validator(cls, key):
|
||||
"""
|
||||
Return the validator for a particular setting.
|
||||
|
||||
If it does not exist, return None
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('validator', None)
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_setting_default(cls, key):
|
||||
"""
|
||||
Return the default value for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
return setting.get('default', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_setting_choices(cls, key):
|
||||
"""
|
||||
Return the validator choices available for a particular setting.
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
if key in cls.GLOBAL_SETTINGS:
|
||||
setting = cls.GLOBAL_SETTINGS[key]
|
||||
choices = setting.get('choices', None)
|
||||
else:
|
||||
choices = None
|
||||
|
||||
"""
|
||||
TODO:
|
||||
if type(choices) is function:
|
||||
# Evaluate the function (we expect it will return a list of tuples...)
|
||||
return choices()
|
||||
"""
|
||||
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_setting_object(cls, key):
|
||||
"""
|
||||
Return an InvenTreeSetting object matching the given key.
|
||||
|
||||
- Key is case-insensitive
|
||||
- Returns None if no match is made
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
try:
|
||||
setting = InvenTreeSetting.objects.filter(key__iexact=key).first()
|
||||
except (ValueError, InvenTreeSetting.DoesNotExist):
|
||||
setting = None
|
||||
except (IntegrityError, OperationalError):
|
||||
setting = None
|
||||
|
||||
# Setting does not exist! (Try to create it)
|
||||
if not setting:
|
||||
|
||||
setting = InvenTreeSetting(key=key, value=InvenTreeSetting.get_setting_default(key))
|
||||
|
||||
try:
|
||||
# Wrap this statement in "atomic", so it can be rolled back if it fails
|
||||
with transaction.atomic():
|
||||
setting.save()
|
||||
except (IntegrityError, OperationalError):
|
||||
# It might be the case that the database isn't created yet
|
||||
pass
|
||||
|
||||
return setting
|
||||
|
||||
@classmethod
|
||||
def get_setting_pk(cls, key):
|
||||
"""
|
||||
Return the primary-key value for a given setting.
|
||||
|
||||
If the setting does not exist, return None
|
||||
"""
|
||||
|
||||
setting = InvenTreeSetting.get_setting_object(cls)
|
||||
|
||||
if setting:
|
||||
return setting.pk
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_setting(cls, key, backup_value=None):
|
||||
"""
|
||||
Get the value of a particular setting.
|
||||
If it does not exist, return the backup value (default = None)
|
||||
"""
|
||||
|
||||
# If no backup value is specified, atttempt to retrieve a "default" value
|
||||
if backup_value is None:
|
||||
backup_value = cls.get_setting_default(key)
|
||||
|
||||
setting = InvenTreeSetting.get_setting_object(key)
|
||||
|
||||
if setting:
|
||||
value = setting.value
|
||||
|
||||
# If the particular setting is defined as a boolean, cast the value to a boolean
|
||||
if setting.is_bool():
|
||||
value = InvenTree.helpers.str2bool(value)
|
||||
|
||||
if setting.is_int():
|
||||
try:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
value = backup_value
|
||||
|
||||
else:
|
||||
value = backup_value
|
||||
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def set_setting(cls, key, value, user, create=True):
|
||||
"""
|
||||
Set the value of a particular setting.
|
||||
If it does not exist, option to create it.
|
||||
|
||||
Args:
|
||||
key: settings key
|
||||
value: New value
|
||||
user: User object (must be staff member to update a core setting)
|
||||
create: If True, create a new setting if the specified key does not exist.
|
||||
"""
|
||||
|
||||
if user is not None and not user.is_staff:
|
||||
return
|
||||
|
||||
try:
|
||||
setting = InvenTreeSetting.objects.get(key__iexact=key)
|
||||
except InvenTreeSetting.DoesNotExist:
|
||||
|
||||
if create:
|
||||
setting = InvenTreeSetting(key=key)
|
||||
else:
|
||||
return
|
||||
|
||||
# Enforce standard boolean representation
|
||||
if setting.is_bool():
|
||||
value = InvenTree.helpers.str2bool(value)
|
||||
|
||||
setting.value = str(value)
|
||||
setting.save()
|
||||
|
||||
key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive'))
|
||||
|
||||
value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return InvenTreeSetting.get_setting_name(self.key)
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
return InvenTreeSetting.get_setting_default(self.key)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return InvenTreeSetting.get_setting_description(self.key)
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return InvenTreeSetting.get_setting_units(self.key)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
If a validator (or multiple validators) are defined for a particular setting key,
|
||||
run them against the 'value' field.
|
||||
"""
|
||||
|
||||
super().clean()
|
||||
|
||||
validator = InvenTreeSetting.get_setting_validator(self.key)
|
||||
|
||||
if self.is_bool():
|
||||
self.value = InvenTree.helpers.str2bool(self.value)
|
||||
|
||||
if self.is_int():
|
||||
try:
|
||||
self.value = int(self.value)
|
||||
except (ValueError):
|
||||
raise ValidationError(_('Must be an integer value'))
|
||||
|
||||
if validator is not None:
|
||||
self.run_validator(validator)
|
||||
|
||||
def run_validator(self, validator):
|
||||
"""
|
||||
Run a validator against the 'value' field for this InvenTreeSetting object.
|
||||
"""
|
||||
|
||||
if validator is None:
|
||||
return
|
||||
|
||||
value = self.value
|
||||
|
||||
# Boolean validator
|
||||
if self.is_bool():
|
||||
# Value must "look like" a boolean value
|
||||
if InvenTree.helpers.is_bool(value):
|
||||
# Coerce into either "True" or "False"
|
||||
value = InvenTree.helpers.str2bool(value)
|
||||
else:
|
||||
raise ValidationError({
|
||||
'value': _('Value must be a boolean value')
|
||||
})
|
||||
|
||||
# Integer validator
|
||||
if self.is_int():
|
||||
|
||||
try:
|
||||
# Coerce into an integer value
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError({
|
||||
'value': _('Value must be an integer value'),
|
||||
})
|
||||
|
||||
# If a list of validators is supplied, iterate through each one
|
||||
if type(validator) in [list, tuple]:
|
||||
for v in validator:
|
||||
self.run_validator(v)
|
||||
|
||||
if callable(validator):
|
||||
# We can accept function validators with a single argument
|
||||
validator(self.value)
|
||||
def get_setting_object(cls, key, user):
|
||||
return super().get_setting_object(key, user=user)
|
||||
|
||||
def validate_unique(self, exclude=None):
|
||||
""" Ensure that the key:value pair is unique.
|
||||
In addition to the base validators, this ensures that the 'key'
|
||||
is unique, using a case-insensitive comparison.
|
||||
"""
|
||||
return super().validate_unique(exclude=exclude, user=self.user)
|
||||
|
||||
super().validate_unique(exclude)
|
||||
|
||||
try:
|
||||
setting = InvenTreeSetting.objects.exclude(id=self.id).filter(key__iexact=self.key)
|
||||
if setting.exists():
|
||||
raise ValidationError({'key': _('Key string must be unique')})
|
||||
except InvenTreeSetting.DoesNotExist:
|
||||
pass
|
||||
|
||||
def choices(self):
|
||||
"""
|
||||
Return the available choices for this setting (or None if no choices are defined)
|
||||
"""
|
||||
|
||||
return InvenTreeSetting.get_setting_choices(self.key)
|
||||
|
||||
def is_bool(self):
|
||||
"""
|
||||
Check if this setting is required to be a boolean value
|
||||
"""
|
||||
|
||||
validator = InvenTreeSetting.get_setting_validator(self.key)
|
||||
|
||||
if validator == bool:
|
||||
return True
|
||||
|
||||
if type(validator) in [list, tuple]:
|
||||
for v in validator:
|
||||
if v == bool:
|
||||
return True
|
||||
|
||||
def as_bool(self):
|
||||
"""
|
||||
Return the value of this setting converted to a boolean value.
|
||||
|
||||
Warning: Only use on values where is_bool evaluates to true!
|
||||
"""
|
||||
|
||||
return InvenTree.helpers.str2bool(self.value)
|
||||
|
||||
def is_int(self):
|
||||
"""
|
||||
Check if the setting is required to be an integer value:
|
||||
"""
|
||||
|
||||
validator = InvenTreeSetting.get_setting_validator(self.key)
|
||||
|
||||
if validator == int:
|
||||
return True
|
||||
|
||||
if type(validator) in [list, tuple]:
|
||||
for v in validator:
|
||||
if v == int:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def as_int(self):
|
||||
"""
|
||||
Return the value of this setting converted to a boolean value.
|
||||
|
||||
If an error occurs, return the default value
|
||||
"""
|
||||
|
||||
try:
|
||||
value = int(self.value)
|
||||
except (ValueError, TypeError):
|
||||
value = self.default_value()
|
||||
|
||||
return value
|
||||
@classmethod
|
||||
def get_filters(cls, key, **kwargs):
|
||||
return {'key__iexact': key, 'user__id__iexact': kwargs['user'].id}
|
||||
|
||||
|
||||
class PriceBreak(models.Model):
|
||||
@ -781,6 +942,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
|
||||
- If MOQ (minimum order quantity) is required, bump quantity
|
||||
- If order multiples are to be observed, then we need to calculate based on that, too
|
||||
"""
|
||||
from common.settings import currency_code_default
|
||||
|
||||
if hasattr(instance, break_name):
|
||||
price_breaks = getattr(instance, break_name).all()
|
||||
@ -804,7 +966,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
|
||||
|
||||
if currency is None:
|
||||
# Default currency selection
|
||||
currency = common.settings.currency_code_default()
|
||||
currency = currency_code_default()
|
||||
|
||||
pb_min = None
|
||||
for pb in price_breaks:
|
||||
|
@ -8,15 +8,19 @@ from __future__ import unicode_literals
|
||||
from moneyed import CURRENCIES
|
||||
from django.conf import settings
|
||||
|
||||
import common.models
|
||||
|
||||
|
||||
def currency_code_default():
|
||||
"""
|
||||
Returns the default currency code (or USD if not specified)
|
||||
"""
|
||||
from django.db.utils import ProgrammingError
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
code = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
try:
|
||||
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
except ProgrammingError:
|
||||
# database is not initialized yet
|
||||
code = ''
|
||||
|
||||
if code not in CURRENCIES:
|
||||
code = 'USD'
|
||||
@ -42,5 +46,6 @@ def stock_expiry_enabled():
|
||||
"""
|
||||
Returns True if the stock expiry feature is enabled
|
||||
"""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
return common.models.InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY')
|
||||
return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY')
|
||||
|
@ -45,8 +45,8 @@ class SettingEdit(AjaxUpdateView):
|
||||
|
||||
ctx['key'] = setting.key
|
||||
ctx['value'] = setting.value
|
||||
ctx['name'] = models.InvenTreeSetting.get_setting_name(setting.key)
|
||||
ctx['description'] = models.InvenTreeSetting.get_setting_description(setting.key)
|
||||
ctx['name'] = self.model.get_setting_name(setting.key)
|
||||
ctx['description'] = self.model.get_setting_description(setting.key)
|
||||
|
||||
return ctx
|
||||
|
||||
@ -69,12 +69,12 @@ class SettingEdit(AjaxUpdateView):
|
||||
self.object.value = str2bool(setting.value)
|
||||
form.fields['value'].value = str2bool(setting.value)
|
||||
|
||||
name = models.InvenTreeSetting.get_setting_name(setting.key)
|
||||
name = self.model.get_setting_name(setting.key)
|
||||
|
||||
if name:
|
||||
form.fields['value'].label = name
|
||||
|
||||
description = models.InvenTreeSetting.get_setting_description(setting.key)
|
||||
description = self.model.get_setting_description(setting.key)
|
||||
|
||||
if description:
|
||||
form.fields['value'].help_text = description
|
||||
@ -111,6 +111,18 @@ class SettingEdit(AjaxUpdateView):
|
||||
form.add_error('value', _('Supplied value must be a boolean'))
|
||||
|
||||
|
||||
class UserSettingEdit(SettingEdit):
|
||||
"""
|
||||
View for editing an InvenTree key:value user settings object,
|
||||
(or creating it if the key does not already exist)
|
||||
"""
|
||||
|
||||
model = models.InvenTreeUserSetting
|
||||
ajax_form_title = _('Change User Setting')
|
||||
form_class = forms.SettingEditForm
|
||||
ajax_template_name = "common/edit_setting.html"
|
||||
|
||||
|
||||
class MultiStepFormView(SessionWizardView):
|
||||
""" Setup basic methods of multi-step form
|
||||
|
||||
|
@ -24,14 +24,14 @@
|
||||
|
||||
{% comment "for later" %}
|
||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Manufacturer Part Stock" %}'>
|
||||
<a href='{% url "manufacturer-part-stock" part.id %}'>
|
||||
<a href='#'>
|
||||
<span class='fas fa-boxes sidebar-icon'></span>
|
||||
{% trans "Stock" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "orders" %}active{% endif %}' title='{% trans "Manufacturer Part Orders" %}'>
|
||||
<a href='{% url "manufacturer-part-orders" part.id %}'>
|
||||
<a href='#'>
|
||||
<span class='fas fa-shopping-cart sidebar-icon'></span>
|
||||
{% trans "Orders" %}
|
||||
</a>
|
||||
|
@ -30,11 +30,9 @@ company_urls = [
|
||||
|
||||
manufacturer_part_urls = [
|
||||
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
||||
])),
|
||||
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
||||
]
|
||||
|
||||
supplier_part_urls = [
|
||||
url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
|
||||
url(r'^(?P<pk>\d+)/', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
|
||||
]
|
||||
|
@ -296,7 +296,9 @@ class StockItemLabel(LabelTemplate):
|
||||
'uid': stock_item.uid,
|
||||
'qr_data': stock_item.format_barcode(brief=True),
|
||||
'qr_url': stock_item.format_barcode(url=True, request=request),
|
||||
'tests': stock_item.testResultMap()
|
||||
'tests': stock_item.testResultMap(),
|
||||
'parameters': stock_item.part.parameters_map(),
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -398,4 +400,5 @@ class PartLabel(LabelTemplate):
|
||||
'revision': part.revision,
|
||||
'qr_data': part.format_barcode(brief=True),
|
||||
'qr_url': part.format_barcode(url=True, request=request),
|
||||
'parameters': part.parameters_map(),
|
||||
}
|
||||
|
@ -84,9 +84,9 @@ class ReceivePurchaseOrderForm(HelperForm):
|
||||
|
||||
location = TreeNodeChoiceField(
|
||||
queryset=StockLocation.objects.all(),
|
||||
required=True,
|
||||
required=False,
|
||||
label=_("Destination"),
|
||||
help_text=_("Receive parts to this location"),
|
||||
help_text=_("Set all received parts listed above to this location (if left blank, use \"Destination\" column value in above table)"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -1,50 +1,74 @@
|
||||
{% extends "order/order_base.html" %}
|
||||
{% extends "order/purchase_order_detail.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Upload File for Purchase Order" %}
|
||||
{{ wizard.form.media }}
|
||||
{% block menubar %}
|
||||
<ul class='list-group'>
|
||||
<li class='list-group-item'>
|
||||
<a href='#' id='po-menu-toggle'>
|
||||
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item' title='{% trans "Return To Order" %}'>
|
||||
<a href='{% url "po-detail" order.id %}' id='select-upload-file' class='nav-toggle'>
|
||||
<span class='fas fa-undo side-icon'></span>
|
||||
{% trans "Return To Order" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
||||
{% block page_content %}
|
||||
|
||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||
{% if description %}- {{ description }}{% endif %}</p>
|
||||
<div class='panel panel-default panel-inventree' id='panel-upload-file'>
|
||||
<div class='panel-heading'>
|
||||
{% block heading %}
|
||||
<h4>{% trans "Upload File for Purchase Order" %}</h4>
|
||||
{{ wizard.form.media }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% block details %}
|
||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
||||
|
||||
{% block form_alert %}
|
||||
{% endblock form_alert %}
|
||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||
{% if description %}- {{ description }}{% endif %}</p>
|
||||
|
||||
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block form_alert %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
{% block form_buttons_top %}
|
||||
{% endblock form_buttons_top %}
|
||||
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||
{{ wizard.management_form }}
|
||||
{% block form_content %}
|
||||
{% crispy wizard.form %}
|
||||
{% endblock form_content %}
|
||||
</table>
|
||||
{% block form_buttons_top %}
|
||||
{% endblock form_buttons_top %}
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
|
||||
</form>
|
||||
{% endblock form_buttons_bottom %}
|
||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||
{{ wizard.management_form }}
|
||||
{% block form_content %}
|
||||
{% crispy wizard.form %}
|
||||
{% endblock form_content %}
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Order is already processed. Files cannot be uploaded." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock details %}
|
||||
{% block form_buttons_bottom %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
|
||||
</form>
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Order is already processed. Files cannot be uploaded." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock details %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
@ -15,14 +15,6 @@
|
||||
{% trans "Order Items" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
||||
<li class='list-group-item' title='{% trans "Upload File" %}'>
|
||||
<a href='{% url "po-upload" order.id %}'>
|
||||
<span class='fas fa-file-upload side-icon'></span>
|
||||
{% trans "Upload File" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class='list-group-item' title='{% trans "Received Stock Items" %}'>
|
||||
<a href='#' id='select-received-items' class='nav-toggle'>
|
||||
<span class='fas fa-sign-in-alt side-icon'></span>
|
||||
|
@ -22,6 +22,9 @@
|
||||
<button type='button' class='btn btn-primary' id='new-po-line'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
|
||||
</button>
|
||||
<a class='btn btn-primary' href='{% url "po-upload" order.id %}' role='button'>
|
||||
<span class='fas fa-file-upload side-icon'></span> {% trans "Upload File" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<label class='control-label'>{% trans "Parts" %}</label>
|
||||
<p class='help-block'>{% trans "Select parts to receive against this order" %}</p>
|
||||
<p class='help-block'>{% trans "Fill out number of parts received, the status and destination" %}</p>
|
||||
|
||||
<table class='table table-striped'>
|
||||
<tr>
|
||||
@ -55,7 +55,14 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ line.get_destination }}
|
||||
<div class='control-group'>
|
||||
<select class='select' name='destination-{{ line.id }}'>
|
||||
<option value="">----------</option>
|
||||
{% for location in stock_locations %}
|
||||
<option value="{{ location.pk }}" {% if location == line.get_destination %}selected="selected"{% endif %}>{{ location }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
||||
@ -67,6 +74,8 @@
|
||||
</table>
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
<div id='form-errors'>{{ form_errors }}</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -393,16 +393,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
p_val = row['data'][p_idx]['cell']
|
||||
|
||||
if p_val:
|
||||
# Delete commas
|
||||
p_val = p_val.replace(',', '')
|
||||
|
||||
try:
|
||||
# Attempt to extract a valid decimal value from the field
|
||||
purchase_price = Decimal(p_val)
|
||||
# Store the 'purchase_price' value
|
||||
row['purchase_price'] = purchase_price
|
||||
except (ValueError, InvalidOperation):
|
||||
pass
|
||||
row['purchase_price'] = p_val
|
||||
|
||||
# Check if there is a column corresponding to "reference"
|
||||
if r_idx >= 0:
|
||||
@ -500,6 +491,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
ctx = {
|
||||
'order': self.order,
|
||||
'lines': self.lines,
|
||||
'stock_locations': StockLocation.objects.all(),
|
||||
}
|
||||
|
||||
return ctx
|
||||
@ -552,6 +544,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
|
||||
self.request = request
|
||||
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
||||
errors = False
|
||||
|
||||
self.lines = []
|
||||
self.destination = None
|
||||
@ -566,12 +559,6 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
except (StockLocation.DoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
errors = False
|
||||
|
||||
if self.destination is None:
|
||||
errors = True
|
||||
msg = _("No destination set")
|
||||
|
||||
# Extract information on all submitted line items
|
||||
for item in request.POST:
|
||||
if item.startswith('line-'):
|
||||
@ -596,6 +583,21 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
else:
|
||||
line.status_code = StockStatus.OK
|
||||
|
||||
# Check the destination field
|
||||
line.destination = None
|
||||
if self.destination:
|
||||
# If global destination is set, overwrite line value
|
||||
line.destination = self.destination
|
||||
else:
|
||||
destination_key = f'destination-{pk}'
|
||||
destination = request.POST.get(destination_key, None)
|
||||
|
||||
if destination:
|
||||
try:
|
||||
line.destination = StockLocation.objects.get(pk=destination)
|
||||
except (StockLocation.DoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
# Check that line matches the order
|
||||
if not line.order == self.order:
|
||||
# TODO - Display a non-field error?
|
||||
@ -654,7 +656,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
|
||||
self.order.receive_line_item(
|
||||
line,
|
||||
self.destination,
|
||||
line.destination,
|
||||
line.receive_quantity,
|
||||
self.request.user,
|
||||
status=line.status_code,
|
||||
|
@ -105,6 +105,20 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Exclude PartCategory tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
cat = PartCategory.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[c.pk for c in cat.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = [
|
||||
@ -361,7 +375,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -619,8 +632,6 @@ class PartList(generics.ListCreateAPIView):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -633,10 +644,6 @@ class PartList(generics.ListCreateAPIView):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Annotate calculated data to the queryset
|
||||
# (This will be used for further filtering)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
# Filter by "uses" query - Limit to parts which use the provided part
|
||||
@ -651,6 +658,20 @@ class PartList(generics.ListCreateAPIView):
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Exclude part variant tree?
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
top_level_part = Part.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[prt.pk for prt in top_level_part.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by 'ancestor'?
|
||||
ancestor = params.get('ancestor', None)
|
||||
|
||||
|
@ -27,6 +27,8 @@ from markdownx.models import MarkdownxField
|
||||
from django_cleanup import cleanup
|
||||
|
||||
from mptt.models import TreeForeignKey, MPTTModel
|
||||
from mptt.exceptions import InvalidMove
|
||||
from mptt.managers import TreeManager
|
||||
|
||||
from stdimage.models import StdImageField
|
||||
|
||||
@ -284,6 +286,24 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False):
|
||||
return matches
|
||||
|
||||
|
||||
class PartManager(TreeManager):
|
||||
"""
|
||||
Defines a custom object manager for the Part model.
|
||||
|
||||
The main purpose of this manager is to reduce the number of database hits,
|
||||
as the Part model has a large number of ForeignKey fields!
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
return super().get_queryset().prefetch_related(
|
||||
'category',
|
||||
'category__parent',
|
||||
'stock_items',
|
||||
'builds',
|
||||
)
|
||||
|
||||
|
||||
@cleanup.ignore
|
||||
class Part(MPTTModel):
|
||||
""" The Part object represents an abstract part, the 'concept' of an actual entity.
|
||||
@ -321,6 +341,8 @@ class Part(MPTTModel):
|
||||
responsible: User who is responsible for this part (optional)
|
||||
"""
|
||||
|
||||
objects = PartManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Part")
|
||||
verbose_name_plural = _("Parts")
|
||||
@ -338,6 +360,17 @@ class Part(MPTTModel):
|
||||
|
||||
return reverse('api-part-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""
|
||||
Return API query filters for limiting field results against this instance
|
||||
"""
|
||||
|
||||
return {
|
||||
'variant_of': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def get_context_data(self, request, **kwargs):
|
||||
"""
|
||||
Return some useful context data about this part for template rendering
|
||||
@ -393,7 +426,12 @@ class Part(MPTTModel):
|
||||
|
||||
self.full_clean()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
try:
|
||||
super().save(*args, **kwargs)
|
||||
except InvalidMove:
|
||||
raise ValidationError({
|
||||
'variant_of': _('Invalid choice for parent part'),
|
||||
})
|
||||
|
||||
if add_category_templates:
|
||||
# Get part category
|
||||
@ -1473,16 +1511,16 @@ class Part(MPTTModel):
|
||||
return self.supplier_parts.count()
|
||||
|
||||
@property
|
||||
def has_pricing_info(self):
|
||||
def has_pricing_info(self, internal=False):
|
||||
""" Return true if there is pricing information for this part """
|
||||
return self.get_price_range() is not None
|
||||
return self.get_price_range(internal=internal) is not None
|
||||
|
||||
@property
|
||||
def has_complete_bom_pricing(self):
|
||||
""" Return true if there is pricing information for each item in the BOM. """
|
||||
|
||||
use_internal = common.models.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
|
||||
for item in self.get_bom_items().all().select_related('sub_part'):
|
||||
if not item.sub_part.has_pricing_info:
|
||||
if not item.sub_part.has_pricing_info(use_internal):
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -1866,6 +1904,23 @@ class Part(MPTTModel):
|
||||
|
||||
return self.parameters.order_by('template__name')
|
||||
|
||||
def parameters_map(self):
|
||||
"""
|
||||
Return a map (dict) of parameter values assocaited with this Part instance,
|
||||
of the form:
|
||||
{
|
||||
"name_1": "value_1",
|
||||
"name_2": "value_2",
|
||||
}
|
||||
"""
|
||||
|
||||
params = {}
|
||||
|
||||
for parameter in self.parameters.all():
|
||||
params[parameter.template.name] = parameter.data
|
||||
|
||||
return params
|
||||
|
||||
@property
|
||||
def has_variants(self):
|
||||
""" Check if this Part object has variants underneath it. """
|
||||
|
@ -215,25 +215,6 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
if category_detail is not True:
|
||||
self.fields.pop('category_detail')
|
||||
|
||||
@staticmethod
|
||||
def prefetch_queryset(queryset):
|
||||
"""
|
||||
Prefetch related database tables,
|
||||
to reduce database hits.
|
||||
"""
|
||||
|
||||
return queryset.prefetch_related(
|
||||
'category',
|
||||
'category__parts',
|
||||
'category__parent',
|
||||
'stock_items',
|
||||
'bom_items',
|
||||
'builds',
|
||||
'supplier_parts',
|
||||
'supplier_parts__purchase_order_line_items',
|
||||
'supplier_parts__purchase_order_line_items__order',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
|
@ -18,7 +18,7 @@ from InvenTree import version, settings
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
from common.models import InvenTreeSetting, ColorTheme
|
||||
from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting
|
||||
from common.settings import currency_code_default
|
||||
|
||||
register = template.Library()
|
||||
@ -69,6 +69,12 @@ def add(x, y, *args, **kwargs):
|
||||
return x + y
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def to_list(*args):
|
||||
""" Return the input arguments as list """
|
||||
return args
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def part_allocation_count(build, part, *args, **kwargs):
|
||||
""" Return the total number of <part> allocated to <build> """
|
||||
@ -182,11 +188,12 @@ def setting_object(key, *args, **kwargs):
|
||||
"""
|
||||
Return a setting object speciifed by the given key
|
||||
(Or return None if the setting does not exist)
|
||||
if a user-setting was requested return that
|
||||
"""
|
||||
|
||||
setting = InvenTreeSetting.get_setting_object(key)
|
||||
|
||||
return setting
|
||||
if 'user' in kwargs:
|
||||
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'])
|
||||
return InvenTreeSetting.get_setting_object(key)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
@ -195,6 +202,8 @@ def settings_value(key, *args, **kwargs):
|
||||
Return a settings value specified by the given key
|
||||
"""
|
||||
|
||||
if 'user' in kwargs:
|
||||
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
||||
return InvenTreeSetting.get_setting(key)
|
||||
|
||||
|
||||
|
@ -99,7 +99,7 @@ class CategoryTest(TestCase):
|
||||
""" Test that the Category parameters are correctly fetched """
|
||||
|
||||
# Check number of SQL queries to iterate other parameters
|
||||
with self.assertNumQueries(3):
|
||||
with self.assertNumQueries(7):
|
||||
# Prefetch: 3 queries (parts, parameters and parameters_template)
|
||||
fasteners = self.fasteners.prefetch_parts_parameters()
|
||||
# Iterate through all parts and parameters
|
||||
|
@ -356,6 +356,7 @@ class TestReport(ReportTemplateBase):
|
||||
'stock_item': stock_item,
|
||||
'serial': stock_item.serial,
|
||||
'part': stock_item.part,
|
||||
'parameters': stock_item.part.parameters_map(),
|
||||
'results': stock_item.testResultMap(include_installed=self.include_installed),
|
||||
'result_list': stock_item.testResultList(include_installed=self.include_installed),
|
||||
'installed_items': stock_item.get_installed_items(cascade=True),
|
||||
|
@ -84,7 +84,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -344,6 +343,20 @@ class StockLocationList(generics.ListCreateAPIView):
|
||||
except (ValueError, StockLocation.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Exclude StockLocation tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
loc = StockLocation.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[subloc.pk for subloc in loc.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, StockLocation.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = [
|
||||
@ -637,7 +650,6 @@ class StockList(generics.ListCreateAPIView):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -721,6 +733,20 @@ class StockList(generics.ListCreateAPIView):
|
||||
if customer:
|
||||
queryset = queryset.filter(customer=customer)
|
||||
|
||||
# Exclude stock item tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
item = StockItem.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[it.pk for it in item.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by 'allocated' parts?
|
||||
allocated = params.get('allocated', None)
|
||||
|
||||
|
@ -23,6 +23,7 @@ from django.dispatch import receiver
|
||||
from markdownx.models import MarkdownxField
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from mptt.managers import TreeManager
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from datetime import datetime, timedelta
|
||||
@ -130,6 +131,31 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
|
||||
child.save()
|
||||
|
||||
|
||||
class StockItemManager(TreeManager):
|
||||
"""
|
||||
Custom database manager for the StockItem class.
|
||||
|
||||
StockItem querysets will automatically prefetch related fields.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
return super().get_queryset().prefetch_related(
|
||||
'belongs_to',
|
||||
'build',
|
||||
'customer',
|
||||
'purchase_order',
|
||||
'sales_order',
|
||||
'supplier_part',
|
||||
'supplier_part__supplier',
|
||||
'allocations',
|
||||
'sales_order_allocations',
|
||||
'location',
|
||||
'part',
|
||||
'tracking_info'
|
||||
)
|
||||
|
||||
|
||||
class StockItem(MPTTModel):
|
||||
"""
|
||||
A StockItem object represents a quantity of physical instances of a part.
|
||||
@ -165,6 +191,17 @@ class StockItem(MPTTModel):
|
||||
def get_api_url():
|
||||
return reverse('api-stock-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""
|
||||
Custom API instance filters
|
||||
"""
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
# A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
|
||||
IN_STOCK_FILTER = Q(
|
||||
quantity__gt=0,
|
||||
|
@ -70,29 +70,6 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
- Includes serialization for the item location
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def prefetch_queryset(queryset):
|
||||
"""
|
||||
Prefetch related database tables,
|
||||
to reduce database hits.
|
||||
"""
|
||||
|
||||
return queryset.prefetch_related(
|
||||
'belongs_to',
|
||||
'build',
|
||||
'customer',
|
||||
'purchase_order',
|
||||
'sales_order',
|
||||
'supplier_part',
|
||||
'supplier_part__supplier',
|
||||
'supplier_part__manufacturer_part__manufacturer',
|
||||
'allocations',
|
||||
'sales_order_allocations',
|
||||
'location',
|
||||
'part',
|
||||
'tracking_info',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
|
@ -272,7 +272,7 @@
|
||||
<tr>
|
||||
<td><span class='fas fa-user-tie'></span></td>
|
||||
<td>{% trans "Customer" %}</td>
|
||||
<td><a href="{% url 'company-detail-assigned-stock' item.customer.id %}">{{ item.customer.name }}</a></td>
|
||||
<td><a href="{% url 'company-detail' item.customer.id %}?display=assigned-stock">{{ item.customer.name }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.belongs_to %}
|
||||
|
@ -93,13 +93,26 @@ function addHeaderAction(label, title, icon, options) {
|
||||
});
|
||||
}
|
||||
|
||||
{% if roles.part.view %}
|
||||
{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %}
|
||||
{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %}
|
||||
{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %}
|
||||
{% to_list setting_part_starred setting_part_latest setting_bom_validation as settings_list_part %}
|
||||
|
||||
{% if roles.part.view and True in settings_list_part %}
|
||||
addHeaderTitle('{% trans "Parts" %}');
|
||||
|
||||
{% if setting_part_starred %}
|
||||
addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star');
|
||||
loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
"starred": true,
|
||||
},
|
||||
name: 'starred_parts',
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_part_latest %}
|
||||
addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper');
|
||||
addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle');
|
||||
|
||||
|
||||
loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
ordering: "-creation_date",
|
||||
@ -107,30 +120,37 @@ loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
|
||||
},
|
||||
name: 'latest_parts',
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
"starred": true,
|
||||
},
|
||||
name: 'starred_parts',
|
||||
});
|
||||
|
||||
{% if setting_bom_validation %}
|
||||
addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle');
|
||||
loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
"bom_valid": false,
|
||||
},
|
||||
name: 'bom_invalid_parts',
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if roles.stock.view %}
|
||||
addHeaderTitle('{% trans "Stock" %}');
|
||||
addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock');
|
||||
addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart');
|
||||
addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times');
|
||||
addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn');
|
||||
{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %}
|
||||
{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %}
|
||||
{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %}
|
||||
{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %}
|
||||
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
|
||||
{% if expiry %}
|
||||
{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %}
|
||||
{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %}
|
||||
{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed setting_stock_expired setting_stock_stale as settings_list_stock %}
|
||||
{% else %}
|
||||
{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed as settings_list_stock %}
|
||||
{% endif %}
|
||||
|
||||
{% if roles.stock.view and True in settings_list_stock %}
|
||||
addHeaderTitle('{% trans "Stock" %}');
|
||||
|
||||
{% if setting_stock_recent %}
|
||||
addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock');
|
||||
loadStockTable($('#table-recently-updated-stock'), {
|
||||
params: {
|
||||
part_detail: true,
|
||||
@ -140,12 +160,43 @@ loadStockTable($('#table-recently-updated-stock'), {
|
||||
name: 'recently-updated-stock',
|
||||
grouping: false,
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_stock_low %}
|
||||
addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart');
|
||||
loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
low_stock: true,
|
||||
},
|
||||
name: "low_stock_parts",
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_stock_depleted %}
|
||||
addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times');
|
||||
loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
depleted_stock: true,
|
||||
},
|
||||
name: "depleted_stock_parts",
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_stock_needed %}
|
||||
addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn');
|
||||
loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
stock_to_build: true,
|
||||
},
|
||||
name: "to_build_parts",
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
|
||||
{% if expiry %}
|
||||
addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times');
|
||||
addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch');
|
||||
|
||||
{% if setting_stock_expired %}
|
||||
addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times');
|
||||
loadStockTable($("#table-expired-stock"), {
|
||||
params: {
|
||||
expired: true,
|
||||
@ -153,7 +204,10 @@ loadStockTable($("#table-expired-stock"), {
|
||||
part_detail: true,
|
||||
},
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_stock_stale %}
|
||||
addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch');
|
||||
loadStockTable($("#table-stale-stock"), {
|
||||
params: {
|
||||
stale: true,
|
||||
@ -164,34 +218,18 @@ loadStockTable($("#table-stale-stock"), {
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
low_stock: true,
|
||||
},
|
||||
name: "low_stock_parts",
|
||||
});
|
||||
|
||||
loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
depleted_stock: true,
|
||||
},
|
||||
name: "depleted_stock_parts",
|
||||
});
|
||||
|
||||
loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", {
|
||||
params: {
|
||||
stock_to_build: true,
|
||||
},
|
||||
name: "to_build_parts",
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if roles.build.view %}
|
||||
addHeaderTitle('{% trans "Build Orders" %}');
|
||||
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
|
||||
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
|
||||
{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %}
|
||||
{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %}
|
||||
{% to_list setting_build_pending setting_build_overdue as settings_list_build %}
|
||||
|
||||
{% if roles.build.view and True in settings_list_build %}
|
||||
addHeaderTitle('{% trans "Build Orders" %}');
|
||||
|
||||
{% if setting_build_pending %}
|
||||
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
|
||||
loadBuildTable("#table-build-pending", {
|
||||
url: "{% url 'api-build-list' %}",
|
||||
params: {
|
||||
@ -199,7 +237,10 @@ loadBuildTable("#table-build-pending", {
|
||||
},
|
||||
disableFilters: true,
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_build_overdue %}
|
||||
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
|
||||
loadBuildTable("#table-build-overdue", {
|
||||
url: "{% url 'api-build-list' %}",
|
||||
params: {
|
||||
@ -209,11 +250,17 @@ loadBuildTable("#table-build-overdue", {
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if roles.purchase_order.view %}
|
||||
addHeaderTitle('{% trans "Purchase Orders" %}');
|
||||
addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt');
|
||||
addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times');
|
||||
{% endif %}
|
||||
|
||||
{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %}
|
||||
{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %}
|
||||
{% to_list setting_po_outstanding setting_po_overdue as settings_list_po %}
|
||||
|
||||
{% if roles.purchase_order.view and True in settings_list_po %}
|
||||
addHeaderTitle('{% trans "Purchase Orders" %}');
|
||||
|
||||
{% if setting_po_outstanding %}
|
||||
addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt');
|
||||
loadPurchaseOrderTable("#table-po-outstanding", {
|
||||
url: "{% url 'api-po-list' %}",
|
||||
params: {
|
||||
@ -221,7 +268,10 @@ loadPurchaseOrderTable("#table-po-outstanding", {
|
||||
outstanding: true,
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_po_overdue %}
|
||||
addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times');
|
||||
loadPurchaseOrderTable("#table-po-overdue", {
|
||||
url: "{% url 'api-po-list' %}",
|
||||
params: {
|
||||
@ -229,14 +279,19 @@ loadPurchaseOrderTable("#table-po-overdue", {
|
||||
overdue: true,
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if roles.sales_order.view %}
|
||||
addHeaderTitle('{% trans "Sales Orders" %}');
|
||||
addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt');
|
||||
addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times');
|
||||
{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %}
|
||||
{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %}
|
||||
{% to_list setting_so_outstanding setting_so_overdue as settings_list_so %}
|
||||
|
||||
{% if roles.sales_order.view and True in settings_list_so %}
|
||||
addHeaderTitle('{% trans "Sales Orders" %}');
|
||||
|
||||
{% if setting_so_outstanding %}
|
||||
addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt');
|
||||
loadSalesOrderTable("#table-so-outstanding", {
|
||||
url: "{% url 'api-so-list' %}",
|
||||
params: {
|
||||
@ -244,7 +299,10 @@ loadSalesOrderTable("#table-so-outstanding", {
|
||||
outstanding: true,
|
||||
},
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if setting_so_overdue %}
|
||||
addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times');
|
||||
loadSalesOrderTable("#table-so-overdue", {
|
||||
url: "{% url 'api-so-list' %}",
|
||||
params: {
|
||||
@ -252,6 +310,7 @@ loadSalesOrderTable("#table-so-overdue", {
|
||||
customer_detail: true,
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
@ -62,6 +62,4 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
@ -31,4 +31,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>{% trans "Search Settings" %}</h4>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" icon="fa-search" %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,12 @@
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% setting_object key as setting %}
|
||||
{% if user_setting %}
|
||||
{% setting_object key user=request.user as setting %}
|
||||
{% else %}
|
||||
{% setting_object key as setting %}
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
{% if icon %}
|
||||
@ -28,7 +33,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class='btn-group float-right'>
|
||||
<button class='btn btn-default btn-glyph btn-edit-setting' pk='{{ setting.pk }}' setting='{{ key }}' title='{% trans "Edit setting" %}'>
|
||||
<button class='btn btn-default btn-glyph btn-edit-setting' pk='{{ setting.pk }}' setting='{{ key }}' title='{% trans "Edit setting" %}' {% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
||||
<span class='fas fa-edit icon-green'></span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -45,9 +45,14 @@
|
||||
$('table').find('.btn-edit-setting').click(function() {
|
||||
var setting = $(this).attr('setting');
|
||||
var pk = $(this).attr('pk');
|
||||
var url = `/settings/${pk}/edit/`;
|
||||
|
||||
if ($(this).attr('user')){
|
||||
url += `user/`;
|
||||
}
|
||||
|
||||
launchModalForm(
|
||||
`/settings/${pk}/edit/`,
|
||||
url,
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
|
@ -8,6 +8,9 @@
|
||||
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-appearance' %}"><span class='fas fa-fill'></span> {% trans "Appearance" %}</a>
|
||||
</li>
|
||||
<li{% ifequal tab 'user_settings' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-user-settings' %}"><span class='fas fa-cog'></span> {% trans "User Settings" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% if user.is_staff %}
|
||||
<h4><span class='fas fa-cogs'></span> {% trans "InvenTree Settings" %}</h4>
|
||||
|
41
InvenTree/templates/InvenTree/settings/user_settings.html
Normal file
41
InvenTree/templates/InvenTree/settings/user_settings.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% extends "InvenTree/settings/settings.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include "InvenTree/settings/tabs.html" with tab='user_settings' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{% trans "User Settings" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block settings %}
|
||||
|
||||
<div class='row'>
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% include "InvenTree/settings/header.html" %}
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %}
|
||||
<tr><td colspan='5'></td></tr>
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_RECENT" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_LOW" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_DEPLETED" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_NEEDED" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_EXPIRED" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_STALE" user_setting=True %}
|
||||
<tr><td colspan='5'></td></tr>
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_PENDING" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_OVERDUE" user_setting=True %}
|
||||
<tr><td colspan='5'></td></tr>
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OUTSTANDING" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OVERDUE" user_setting=True %}
|
||||
<tr><td colspan='5'></td></tr>
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OUTSTANDING" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OVERDUE" user_setting=True %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
@ -142,11 +142,11 @@
|
||||
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
||||
|
||||
<!-- general InvenTree -->
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
|
||||
|
||||
<!-- translated -->
|
||||
<script type='text/javascript' src="{% i18n_static 'inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
|
||||
|
@ -350,6 +350,12 @@ function constructFormBody(fields, options) {
|
||||
for(field in fields) {
|
||||
fields[field].name = field;
|
||||
|
||||
|
||||
// If any "instance_filters" are defined for the endpoint, copy them across (overwrite)
|
||||
if (fields[field].instance_filters) {
|
||||
fields[field].filters = Object.assign(fields[field].filters || {}, fields[field].instance_filters);
|
||||
}
|
||||
|
||||
var field_options = displayed_fields[field];
|
||||
|
||||
// Copy custom options across to the fields object
|
||||
|
@ -1,3 +1,5 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
function attachClipboard(selector, containerselector, textElement) {
|
||||
// set container
|
||||
if (containerselector){
|
||||
@ -13,7 +15,8 @@ function attachClipboard(selector, containerselector, textElement) {
|
||||
}
|
||||
} else {
|
||||
text = function(trigger) {
|
||||
var content = trigger.parentElement.parentElement.textContent;return content.trim();
|
||||
var content = trigger.parentElement.parentElement.textContent;
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +83,45 @@ function inventreeDocReady() {
|
||||
attachClipboard('.clip-btn');
|
||||
attachClipboard('.clip-btn', 'modal-about'); // modals
|
||||
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text'); // version-text
|
||||
|
||||
// Add autocomplete to the search-bar
|
||||
$("#search-bar" ).autocomplete({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: '/api/part/',
|
||||
data: {
|
||||
search: request.term,
|
||||
limit: {% settings_value 'SEARCH_PREVIEW_RESULTS' %},
|
||||
offset: 0
|
||||
},
|
||||
success: function (data) {
|
||||
var transformed = $.map(data.results, function (el) {
|
||||
return {
|
||||
label: el.name,
|
||||
id: el.pk,
|
||||
thumbnail: el.thumbnail
|
||||
};
|
||||
});
|
||||
response(transformed);
|
||||
},
|
||||
error: function () {
|
||||
response([]);
|
||||
}
|
||||
});
|
||||
},
|
||||
create: function () {
|
||||
$(this).data('ui-autocomplete')._renderItem = function (ul, item) {
|
||||
return $('<li>')
|
||||
.append('<span>' + imageHoverIcon(item.thumbnail) + item.label + '</span>')
|
||||
.appendTo(ul);
|
||||
};
|
||||
},
|
||||
select: function( event, ui ) {
|
||||
window.location = '/part/' + ui.item.id + '/';
|
||||
},
|
||||
minLength: 2,
|
||||
classes: {'ui-autocomplete': 'dropdown-menu search-menu'},
|
||||
});
|
||||
}
|
||||
|
||||
function isFileTransfer(transfer) {
|
@ -3,7 +3,7 @@
|
||||
<form class="navbar-form navbar-left" action="{% url 'search' %}" method='post'>
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<input type="text" name='search' class="form-control" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
|
||||
<input type="text" name='search' class="form-control" id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
|
||||
</div>
|
||||
<button type="submit" id='search-submit' class="btn btn-default" title='{% trans "Search" %}'>
|
||||
<span class='fas fa-search'></span>
|
||||
|
@ -141,6 +141,7 @@ class RuleSet(models.Model):
|
||||
# Models which currently do not require permissions
|
||||
'common_colortheme',
|
||||
'common_inventreesetting',
|
||||
'common_inventreeusersetting',
|
||||
'company_contact',
|
||||
'users_owner',
|
||||
|
||||
|
@ -34,15 +34,6 @@ InvenTree is supported by a [companion mobile app](https://inventree.readthedocs
|
||||
|
||||
# Translation
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Native language translation of the InvenTree web application is [community contributed via crowdin](https://crowdin.com/project/inventree). **Contributions are welcomed and encouraged**.
|
||||
|
||||
To contribute to the translation effort, navigate to the [InvenTree crowdin project](https://crowdin.com/project/inventree), create a free account, and start making translations suggestions for your language of choice!
|
||||
|
Reference in New Issue
Block a user