Merge branch 'master' of https://github.com/inventree/InvenTree into homepage-settings
@ -20,7 +20,6 @@ from djmoney.forms.fields import MoneyField
|
|||||||
from djmoney.models.validators import MinMoneyValidator
|
from djmoney.models.validators import MinMoneyValidator
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import common.settings
|
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeURLFormField(FormURLField):
|
class InvenTreeURLFormField(FormURLField):
|
||||||
@ -42,9 +41,11 @@ class InvenTreeURLField(models.URLField):
|
|||||||
|
|
||||||
def money_kwargs():
|
def money_kwargs():
|
||||||
""" returns the database settings for MoneyFields """
|
""" returns the database settings for MoneyFields """
|
||||||
|
from common.settings import currency_code_mappings, currency_code_default
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
kwargs['currency_choices'] = common.settings.currency_code_mappings()
|
kwargs['currency_choices'] = currency_code_mappings()
|
||||||
kwargs['default_currency'] = common.settings.currency_code_default
|
kwargs['default_currency'] = currency_code_default()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
@ -631,13 +631,34 @@ def clean_decimal(number):
|
|||||||
""" Clean-up decimal value """
|
""" Clean-up decimal value """
|
||||||
|
|
||||||
# Check if empty
|
# Check if empty
|
||||||
if number is None or number == '':
|
if number is None or number == '' or number == 0:
|
||||||
return Decimal(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:
|
try:
|
||||||
clean_number = Decimal(number)
|
clean_number = Decimal(number)
|
||||||
except InvalidOperation:
|
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()
|
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):
|
def determine_metadata(self, request, view):
|
||||||
|
|
||||||
|
self.request = request
|
||||||
|
self.view = view
|
||||||
|
|
||||||
metadata = super().determine_metadata(request, view)
|
metadata = super().determine_metadata(request, view)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
@ -136,6 +139,42 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
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
|
return serializer_info
|
||||||
|
|
||||||
def get_field_info(self, field):
|
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
|
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):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -1037,3 +1037,10 @@ a.anchor {
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-menu {
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-menu .ui-menu-item {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@ -59,6 +59,11 @@
|
|||||||
<h1>YOUR COMPONENTS:</h1>
|
<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 -->
|
<!-- Highlight / Error -->
|
||||||
@ -270,6 +292,33 @@
|
|||||||
<script src="jquery-ui.js"></script>
|
<script src="jquery-ui.js"></script>
|
||||||
<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
|
* 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
|
* 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 */
|
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||||
|
|
||||||
@ -160,6 +160,66 @@
|
|||||||
right: -5px;
|
right: -5px;
|
||||||
top: -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
|
/* Component containers
|
||||||
----------------------------------*/
|
----------------------------------*/
|
||||||
|
1915
InvenTree/InvenTree/static/script/jquery-ui/jquery-ui.js
vendored
@ -164,3 +164,63 @@
|
|||||||
right: -5px;
|
right: -5px;
|
||||||
top: -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
|
* http://jqueryui.com
|
||||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
* 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
|
* http://jqueryui.com
|
||||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||||
|
|
||||||
|
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)
|
@ -112,6 +112,7 @@ dynamic_javascript_urls = [
|
|||||||
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
|
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'^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'^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'^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'^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'),
|
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
|
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"
|
# Filter by "parent"
|
||||||
parent = params.get('parent', None)
|
parent = params.get('parent', None)
|
||||||
|
|
||||||
|
@ -96,6 +96,14 @@ class Build(MPTTModel):
|
|||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-build-list')
|
return reverse('api-build-list')
|
||||||
|
|
||||||
|
def api_instance_filters(self):
|
||||||
|
|
||||||
|
return {
|
||||||
|
'parent': {
|
||||||
|
'exclude_tree': self.pk,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -53,17 +53,20 @@ class FileManager:
|
|||||||
|
|
||||||
ext = os.path.splitext(file.name)[-1].lower().replace('.', '')
|
ext = os.path.splitext(file.name)[-1].lower().replace('.', '')
|
||||||
|
|
||||||
if ext in ['csv', 'tsv', ]:
|
try:
|
||||||
# These file formats need string decoding
|
if ext in ['csv', 'tsv', ]:
|
||||||
raw_data = file.read().decode('utf-8')
|
# These file formats need string decoding
|
||||||
# Reset stream position to beginning of file
|
raw_data = file.read().decode('utf-8')
|
||||||
file.seek(0)
|
# Reset stream position to beginning of file
|
||||||
elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
|
file.seek(0)
|
||||||
raw_data = file.read()
|
elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
|
||||||
# Reset stream position to beginning of file
|
raw_data = file.read()
|
||||||
file.seek(0)
|
# Reset stream position to beginning of file
|
||||||
else:
|
file.seek(0)
|
||||||
raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
|
else:
|
||||||
|
raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise ValidationError(_('Error reading file (invalid encoding)'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cleaned_data = tablib.Dataset().load(raw_data, format=ext)
|
cleaned_data = tablib.Dataset().load(raw_data, format=ext)
|
||||||
|
@ -19,8 +19,6 @@ from djmoney.settings import CURRENCY_CHOICES
|
|||||||
from djmoney.contrib.exchange.models import convert_money
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||||
|
|
||||||
import common.settings
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator, URLValidator
|
from django.core.validators import MinValueValidator, URLValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -670,6 +668,13 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'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': {
|
'STOCK_ENABLE_EXPIRY': {
|
||||||
'name': _('Stock Expiry'),
|
'name': _('Stock Expiry'),
|
||||||
'description': _('Enable stock expiry functionality'),
|
'description': _('Enable stock expiry functionality'),
|
||||||
@ -853,6 +858,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
|
|||||||
- If MOQ (minimum order quantity) is required, bump quantity
|
- 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
|
- 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):
|
if hasattr(instance, break_name):
|
||||||
price_breaks = getattr(instance, break_name).all()
|
price_breaks = getattr(instance, break_name).all()
|
||||||
@ -876,7 +882,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
|
|||||||
|
|
||||||
if currency is None:
|
if currency is None:
|
||||||
# Default currency selection
|
# Default currency selection
|
||||||
currency = common.settings.currency_code_default()
|
currency = currency_code_default()
|
||||||
|
|
||||||
pb_min = None
|
pb_min = None
|
||||||
for pb in price_breaks:
|
for pb in price_breaks:
|
||||||
|
@ -8,15 +8,19 @@ from __future__ import unicode_literals
|
|||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
import common.models
|
|
||||||
|
|
||||||
|
|
||||||
def currency_code_default():
|
def currency_code_default():
|
||||||
"""
|
"""
|
||||||
Returns the default currency code (or USD if not specified)
|
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:
|
if code not in CURRENCIES:
|
||||||
code = 'USD'
|
code = 'USD'
|
||||||
@ -42,5 +46,6 @@ def stock_expiry_enabled():
|
|||||||
"""
|
"""
|
||||||
Returns True if the stock expiry feature is 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')
|
||||||
|
@ -24,14 +24,14 @@
|
|||||||
|
|
||||||
{% comment "for later" %}
|
{% comment "for later" %}
|
||||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Manufacturer Part Stock" %}'>
|
<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>
|
<span class='fas fa-boxes sidebar-icon'></span>
|
||||||
{% trans "Stock" %}
|
{% trans "Stock" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class='list-group-item {% if tab == "orders" %}active{% endif %}' title='{% trans "Manufacturer Part Orders" %}'>
|
<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>
|
<span class='fas fa-shopping-cart sidebar-icon'></span>
|
||||||
{% trans "Orders" %}
|
{% trans "Orders" %}
|
||||||
</a>
|
</a>
|
||||||
|
@ -30,11 +30,9 @@ company_urls = [
|
|||||||
|
|
||||||
manufacturer_part_urls = [
|
manufacturer_part_urls = [
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/', include([
|
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
||||||
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
|
||||||
])),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
supplier_part_urls = [
|
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,
|
'uid': stock_item.uid,
|
||||||
'qr_data': stock_item.format_barcode(brief=True),
|
'qr_data': stock_item.format_barcode(brief=True),
|
||||||
'qr_url': stock_item.format_barcode(url=True, request=request),
|
'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,
|
'revision': part.revision,
|
||||||
'qr_data': part.format_barcode(brief=True),
|
'qr_data': part.format_barcode(brief=True),
|
||||||
'qr_url': part.format_barcode(url=True, request=request),
|
'qr_url': part.format_barcode(url=True, request=request),
|
||||||
|
'parameters': part.parameters_map(),
|
||||||
}
|
}
|
||||||
|
@ -84,9 +84,9 @@ class ReceivePurchaseOrderForm(HelperForm):
|
|||||||
|
|
||||||
location = TreeNodeChoiceField(
|
location = TreeNodeChoiceField(
|
||||||
queryset=StockLocation.objects.all(),
|
queryset=StockLocation.objects.all(),
|
||||||
required=True,
|
required=False,
|
||||||
label=_("Destination"),
|
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:
|
class Meta:
|
||||||
|
@ -1,50 +1,74 @@
|
|||||||
{% extends "order/order_base.html" %}
|
{% extends "order/purchase_order_detail.html" %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block menubar %}
|
||||||
{% trans "Upload File for Purchase Order" %}
|
<ul class='list-group'>
|
||||||
{{ wizard.form.media }}
|
<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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block page_content %}
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
|
|
||||||
|
|
||||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
<div class='panel panel-default panel-inventree' id='panel-upload-file'>
|
||||||
{% if description %}- {{ description }}{% endif %}</p>
|
<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 %}
|
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||||
{% endblock form_alert %}
|
{% if description %}- {{ description }}{% endif %}</p>
|
||||||
|
|
||||||
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
|
{% block form_alert %}
|
||||||
{% csrf_token %}
|
{% endblock form_alert %}
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block form_buttons_top %}
|
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
|
||||||
{% endblock form_buttons_top %}
|
{% csrf_token %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
{% block form_buttons_top %}
|
||||||
{{ wizard.management_form }}
|
{% endblock form_buttons_top %}
|
||||||
{% block form_content %}
|
|
||||||
{% crispy wizard.form %}
|
|
||||||
{% endblock form_content %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% block form_buttons_bottom %}
|
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||||
{% if wizard.steps.prev %}
|
{{ wizard.management_form }}
|
||||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
{% block form_content %}
|
||||||
{% endif %}
|
{% crispy wizard.form %}
|
||||||
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
|
{% endblock form_content %}
|
||||||
</form>
|
</table>
|
||||||
{% endblock form_buttons_bottom %}
|
|
||||||
|
|
||||||
{% else %}
|
{% block form_buttons_bottom %}
|
||||||
<div class='alert alert-danger alert-block' role='alert'>
|
{% if wizard.steps.prev %}
|
||||||
{% trans "Order is already processed. Files cannot be uploaded." %}
|
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
|
||||||
{% endblock details %}
|
</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 js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
@ -15,14 +15,6 @@
|
|||||||
{% trans "Order Items" %}
|
{% trans "Order Items" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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" %}'>
|
<li class='list-group-item' title='{% trans "Received Stock Items" %}'>
|
||||||
<a href='#' id='select-received-items' class='nav-toggle'>
|
<a href='#' id='select-received-items' class='nav-toggle'>
|
||||||
<span class='fas fa-sign-in-alt side-icon'></span>
|
<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'>
|
<button type='button' class='btn btn-primary' id='new-po-line'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
|
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
|
||||||
</button>
|
</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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
<label class='control-label'>{% trans "Parts" %}</label>
|
<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'>
|
<table class='table table-striped'>
|
||||||
<tr>
|
<tr>
|
||||||
@ -55,7 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
||||||
<td>
|
<td>
|
||||||
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
<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>
|
</table>
|
||||||
|
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
|
|
||||||
|
<div id='form-errors'>{{ form_errors }}</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -393,16 +393,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
|||||||
p_val = row['data'][p_idx]['cell']
|
p_val = row['data'][p_idx]['cell']
|
||||||
|
|
||||||
if p_val:
|
if p_val:
|
||||||
# Delete commas
|
row['purchase_price'] = p_val
|
||||||
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
|
|
||||||
|
|
||||||
# Check if there is a column corresponding to "reference"
|
# Check if there is a column corresponding to "reference"
|
||||||
if r_idx >= 0:
|
if r_idx >= 0:
|
||||||
@ -500,6 +491,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
|||||||
ctx = {
|
ctx = {
|
||||||
'order': self.order,
|
'order': self.order,
|
||||||
'lines': self.lines,
|
'lines': self.lines,
|
||||||
|
'stock_locations': StockLocation.objects.all(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
@ -552,6 +544,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
|||||||
|
|
||||||
self.request = request
|
self.request = request
|
||||||
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
||||||
|
errors = False
|
||||||
|
|
||||||
self.lines = []
|
self.lines = []
|
||||||
self.destination = None
|
self.destination = None
|
||||||
@ -566,12 +559,6 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
|||||||
except (StockLocation.DoesNotExist, ValueError):
|
except (StockLocation.DoesNotExist, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
errors = False
|
|
||||||
|
|
||||||
if self.destination is None:
|
|
||||||
errors = True
|
|
||||||
msg = _("No destination set")
|
|
||||||
|
|
||||||
# Extract information on all submitted line items
|
# Extract information on all submitted line items
|
||||||
for item in request.POST:
|
for item in request.POST:
|
||||||
if item.startswith('line-'):
|
if item.startswith('line-'):
|
||||||
@ -596,6 +583,21 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
|||||||
else:
|
else:
|
||||||
line.status_code = StockStatus.OK
|
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
|
# Check that line matches the order
|
||||||
if not line.order == self.order:
|
if not line.order == self.order:
|
||||||
# TODO - Display a non-field error?
|
# TODO - Display a non-field error?
|
||||||
@ -654,7 +656,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
|||||||
|
|
||||||
self.order.receive_line_item(
|
self.order.receive_line_item(
|
||||||
line,
|
line,
|
||||||
self.destination,
|
line.destination,
|
||||||
line.receive_quantity,
|
line.receive_quantity,
|
||||||
self.request.user,
|
self.request.user,
|
||||||
status=line.status_code,
|
status=line.status_code,
|
||||||
|
@ -105,6 +105,20 @@ class CategoryList(generics.ListCreateAPIView):
|
|||||||
except (ValueError, PartCategory.DoesNotExist):
|
except (ValueError, PartCategory.DoesNotExist):
|
||||||
pass
|
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
|
return queryset
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
@ -361,7 +375,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
|
|
||||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -619,8 +632,6 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
|
|
||||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -633,10 +644,6 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
params = self.request.query_params
|
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)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
# Filter by "uses" query - Limit to parts which use the provided part
|
# Filter by "uses" query - Limit to parts which use the provided part
|
||||||
@ -651,6 +658,20 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
except (ValueError, Part.DoesNotExist):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
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'?
|
# Filter by 'ancestor'?
|
||||||
ancestor = params.get('ancestor', None)
|
ancestor = params.get('ancestor', None)
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ from markdownx.models import MarkdownxField
|
|||||||
from django_cleanup import cleanup
|
from django_cleanup import cleanup
|
||||||
|
|
||||||
from mptt.models import TreeForeignKey, MPTTModel
|
from mptt.models import TreeForeignKey, MPTTModel
|
||||||
|
from mptt.exceptions import InvalidMove
|
||||||
|
from mptt.managers import TreeManager
|
||||||
|
|
||||||
from stdimage.models import StdImageField
|
from stdimage.models import StdImageField
|
||||||
|
|
||||||
@ -284,6 +286,24 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False):
|
|||||||
return matches
|
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
|
@cleanup.ignore
|
||||||
class Part(MPTTModel):
|
class Part(MPTTModel):
|
||||||
""" The Part object represents an abstract part, the 'concept' of an actual entity.
|
""" 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)
|
responsible: User who is responsible for this part (optional)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
objects = PartManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Part")
|
verbose_name = _("Part")
|
||||||
verbose_name_plural = _("Parts")
|
verbose_name_plural = _("Parts")
|
||||||
@ -338,6 +360,17 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
return reverse('api-part-list')
|
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):
|
def get_context_data(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return some useful context data about this part for template rendering
|
Return some useful context data about this part for template rendering
|
||||||
@ -393,7 +426,12 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
self.full_clean()
|
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:
|
if add_category_templates:
|
||||||
# Get part category
|
# Get part category
|
||||||
@ -1473,16 +1511,16 @@ class Part(MPTTModel):
|
|||||||
return self.supplier_parts.count()
|
return self.supplier_parts.count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_pricing_info(self):
|
def has_pricing_info(self, internal=False):
|
||||||
""" Return true if there is pricing information for this part """
|
""" 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
|
@property
|
||||||
def has_complete_bom_pricing(self):
|
def has_complete_bom_pricing(self):
|
||||||
""" Return true if there is pricing information for each item in the BOM. """
|
""" 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'):
|
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 False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -1866,6 +1904,23 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
return self.parameters.order_by('template__name')
|
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
|
@property
|
||||||
def has_variants(self):
|
def has_variants(self):
|
||||||
""" Check if this Part object has variants underneath it. """
|
""" Check if this Part object has variants underneath it. """
|
||||||
|
@ -215,25 +215,6 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
if category_detail is not True:
|
if category_detail is not True:
|
||||||
self.fields.pop('category_detail')
|
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
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""
|
"""
|
||||||
|
@ -99,7 +99,7 @@ class CategoryTest(TestCase):
|
|||||||
""" Test that the Category parameters are correctly fetched """
|
""" Test that the Category parameters are correctly fetched """
|
||||||
|
|
||||||
# Check number of SQL queries to iterate other parameters
|
# 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)
|
# Prefetch: 3 queries (parts, parameters and parameters_template)
|
||||||
fasteners = self.fasteners.prefetch_parts_parameters()
|
fasteners = self.fasteners.prefetch_parts_parameters()
|
||||||
# Iterate through all parts and parameters
|
# Iterate through all parts and parameters
|
||||||
|
@ -356,6 +356,7 @@ class TestReport(ReportTemplateBase):
|
|||||||
'stock_item': stock_item,
|
'stock_item': stock_item,
|
||||||
'serial': stock_item.serial,
|
'serial': stock_item.serial,
|
||||||
'part': stock_item.part,
|
'part': stock_item.part,
|
||||||
|
'parameters': stock_item.part.parameters_map(),
|
||||||
'results': stock_item.testResultMap(include_installed=self.include_installed),
|
'results': stock_item.testResultMap(include_installed=self.include_installed),
|
||||||
'result_list': stock_item.testResultList(include_installed=self.include_installed),
|
'result_list': stock_item.testResultList(include_installed=self.include_installed),
|
||||||
'installed_items': stock_item.get_installed_items(cascade=True),
|
'installed_items': stock_item.get_installed_items(cascade=True),
|
||||||
|
@ -84,7 +84,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
|
||||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
queryset = StockItemSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -344,6 +343,20 @@ class StockLocationList(generics.ListCreateAPIView):
|
|||||||
except (ValueError, StockLocation.DoesNotExist):
|
except (ValueError, StockLocation.DoesNotExist):
|
||||||
pass
|
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
|
return queryset
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
@ -637,7 +650,6 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
|
||||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
queryset = StockItemSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -721,6 +733,20 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if customer:
|
if customer:
|
||||||
queryset = queryset.filter(customer=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?
|
# Filter by 'allocated' parts?
|
||||||
allocated = params.get('allocated', None)
|
allocated = params.get('allocated', None)
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from django.dispatch import receiver
|
|||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
|
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
from mptt.managers import TreeManager
|
||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -130,6 +131,31 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
|
|||||||
child.save()
|
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):
|
class StockItem(MPTTModel):
|
||||||
"""
|
"""
|
||||||
A StockItem object represents a quantity of physical instances of a part.
|
A StockItem object represents a quantity of physical instances of a part.
|
||||||
@ -165,6 +191,17 @@ class StockItem(MPTTModel):
|
|||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-stock-list')
|
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"
|
# A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
|
||||||
IN_STOCK_FILTER = Q(
|
IN_STOCK_FILTER = Q(
|
||||||
quantity__gt=0,
|
quantity__gt=0,
|
||||||
|
@ -70,29 +70,6 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
- Includes serialization for the item location
|
- 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
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""
|
"""
|
||||||
|
@ -272,7 +272,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-user-tie'></span></td>
|
<td><span class='fas fa-user-tie'></span></td>
|
||||||
<td>{% trans "Customer" %}</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>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.belongs_to %}
|
{% if item.belongs_to %}
|
||||||
|
@ -31,4 +31,11 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 %}
|
{% endblock %}
|
||||||
|
@ -142,11 +142,11 @@
|
|||||||
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
||||||
|
|
||||||
<!-- general InvenTree -->
|
<!-- 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/notification.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
|
||||||
|
|
||||||
<!-- translated -->
|
<!-- 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 'api.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
|
||||||
|
@ -350,6 +350,12 @@ function constructFormBody(fields, options) {
|
|||||||
for(field in fields) {
|
for(field in fields) {
|
||||||
fields[field].name = field;
|
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];
|
var field_options = displayed_fields[field];
|
||||||
|
|
||||||
// Copy custom options across to the fields object
|
// Copy custom options across to the fields object
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
function attachClipboard(selector, containerselector, textElement) {
|
function attachClipboard(selector, containerselector, textElement) {
|
||||||
// set container
|
// set container
|
||||||
if (containerselector){
|
if (containerselector){
|
||||||
@ -13,7 +15,8 @@ function attachClipboard(selector, containerselector, textElement) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
text = function(trigger) {
|
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');
|
||||||
attachClipboard('.clip-btn', 'modal-about'); // modals
|
attachClipboard('.clip-btn', 'modal-about'); // modals
|
||||||
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text'); // version-text
|
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) {
|
function isFileTransfer(transfer) {
|
@ -3,7 +3,7 @@
|
|||||||
<form class="navbar-form navbar-left" action="{% url 'search' %}" method='post'>
|
<form class="navbar-form navbar-left" action="{% url 'search' %}" method='post'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-group">
|
<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>
|
</div>
|
||||||
<button type="submit" id='search-submit' class="btn btn-default" title='{% trans "Search" %}'>
|
<button type="submit" id='search-submit' class="btn btn-default" title='{% trans "Search" %}'>
|
||||||
<span class='fas fa-search'></span>
|
<span class='fas fa-search'></span>
|
||||||
|
@ -34,15 +34,6 @@ InvenTree is supported by a [companion mobile app](https://inventree.readthedocs
|
|||||||
|
|
||||||
# Translation
|
# Translation
|
||||||
|
|
||||||
![de translation](https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=flat&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
![es-ES translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es-ES&style=flat&query=%24.progress.1.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
![it translation](https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=flat&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pl&style=flat&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
![tr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=tr&style=flat&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
![zh-CN translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-CN&style=flat&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
|
|
||||||
|
|
||||||
Native language translation of the InvenTree web application is [community contributed via crowdin](https://crowdin.com/project/inventree). **Contributions are welcomed and encouraged**.
|
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!
|
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!
|
||||||
|