Merge remote-tracking branch 'inventree/master'
@ -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)
|
@ -111,6 +111,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:
|
||||||
|
@ -277,6 +277,13 @@ class InvenTreeSetting(models.Model):
|
|||||||
'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'),
|
||||||
|
@ -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:
|
||||||
|
@ -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 %}
|
@ -491,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
|
||||||
@ -543,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
|
||||||
@ -557,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-'):
|
||||||
@ -587,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?
|
||||||
@ -645,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
|
||||||
@ -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>
|
||||||
|