mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
9b0825739a
@ -41,6 +41,17 @@ class InvenTreeTree(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
related_name='children')
|
related_name='children')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_count(self):
|
||||||
|
""" Return the number of items which exist *under* this node in the tree.
|
||||||
|
|
||||||
|
Here an 'item' is considered to be the 'leaf' at the end of each branch,
|
||||||
|
and the exact nature here will depend on the class implementation.
|
||||||
|
|
||||||
|
The default implementation returns zero
|
||||||
|
"""
|
||||||
|
return 0
|
||||||
|
|
||||||
def getUniqueParents(self, unique=None):
|
def getUniqueParents(self, unique=None):
|
||||||
""" Return a flat set of all parent items that exist above this node.
|
""" Return a flat set of all parent items that exist above this node.
|
||||||
If any parents are repeated (which would be very bad!), the process is halted
|
If any parents are repeated (which would be very bad!), the process is halted
|
||||||
|
@ -28,11 +28,22 @@ class TreeSerializer(views.APIView):
|
|||||||
Ref: https://github.com/jonmiles/bootstrap-treeview
|
Ref: https://github.com/jonmiles/bootstrap-treeview
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_url(self):
|
||||||
|
""" Return the root URL for the tree. Implementation is class dependent.
|
||||||
|
|
||||||
|
Default implementation returns #
|
||||||
|
"""
|
||||||
|
|
||||||
|
return '#'
|
||||||
|
|
||||||
def itemToJson(self, item):
|
def itemToJson(self, item):
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
'pk': item.id,
|
||||||
'text': item.name,
|
'text': item.name,
|
||||||
'href': item.get_absolute_url(),
|
'href': item.get_absolute_url(),
|
||||||
|
'tags': [item.item_count],
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.has_children:
|
if item.has_children:
|
||||||
@ -51,12 +62,18 @@ class TreeSerializer(views.APIView):
|
|||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
|
top_count = 0
|
||||||
|
|
||||||
for item in top_items:
|
for item in top_items:
|
||||||
nodes.append(self.itemToJson(item))
|
nodes.append(self.itemToJson(item))
|
||||||
|
top_count += item.item_count
|
||||||
|
|
||||||
top = {
|
top = {
|
||||||
|
'pk': None,
|
||||||
'text': self.title,
|
'text': self.title,
|
||||||
|
'href': self.root_url,
|
||||||
'nodes': nodes,
|
'nodes': nodes,
|
||||||
|
'tags': [top_count],
|
||||||
}
|
}
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
|
@ -14,6 +14,7 @@ from rest_framework import generics, permissions
|
|||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from .models import Part, PartCategory, BomItem, PartStar
|
from .models import Part, PartCategory, BomItem, PartStar
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
@ -31,6 +32,10 @@ class PartCategoryTree(TreeSerializer):
|
|||||||
title = "Parts"
|
title = "Parts"
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_url(self):
|
||||||
|
return reverse('part-index')
|
||||||
|
|
||||||
|
|
||||||
class CategoryList(generics.ListCreateAPIView):
|
class CategoryList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PartCategory objects.
|
""" API endpoint for accessing a list of PartCategory objects.
|
||||||
|
@ -47,18 +47,18 @@ class PartCategory(InvenTreeTree):
|
|||||||
verbose_name = "Part Category"
|
verbose_name = "Part Category"
|
||||||
verbose_name_plural = "Part Categories"
|
verbose_name_plural = "Part Categories"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_count(self):
|
||||||
|
return self.partcount
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def partcount(self):
|
def partcount(self):
|
||||||
""" Return the total part count under this category
|
""" Return the total part count under this category
|
||||||
(including children of child categories)
|
(including children of child categories)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
count = self.parts.count()
|
return len(Part.objects.filter(category__in=self.getUniqueChildren(),
|
||||||
|
active=True))
|
||||||
for child in self.children.all():
|
|
||||||
count += child.partcount
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_parts(self):
|
def has_parts(self):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class='navigation'>
|
<div class='navigation'>
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href='#' id='toggle-part-tree'><b>+</b></a></li>
|
<li><a href='#' id='toggle-part-tree'><b><span class='glyphicon glyphicon-small glyphicon-th-list'></span></b></a></li>
|
||||||
<li class="breadcrumb-item{% if category is None %} active" aria-current="page{% endif %}"><a href="/part/">Parts</a></li>
|
<li class="breadcrumb-item{% if category is None %} active" aria-current="page{% endif %}"><a href="/part/">Parts</a></li>
|
||||||
{% if category %}
|
{% if category %}
|
||||||
{% for path_item in category.parentpath %}
|
{% for path_item in category.parentpath %}
|
||||||
|
@ -34,7 +34,11 @@ InvenTree | Part List
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
loadTree("{% url 'api-part-tree' %}",
|
loadTree("{% url 'api-part-tree' %}",
|
||||||
"#part-tree");
|
"#part-tree",
|
||||||
|
{
|
||||||
|
name: 'part',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
$("#toggle-part-tree").click(function() {
|
$("#toggle-part-tree").click(function() {
|
||||||
toggleSideNav("#sidenav");
|
toggleSideNav("#sidenav");
|
||||||
|
@ -19,6 +19,14 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.treeview .badge {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeview .list-group-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Force select2 elements in modal forms to be full width */
|
/* Force select2 elements in modal forms to be full width */
|
||||||
.select-full-width {
|
.select-full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -113,13 +121,26 @@
|
|||||||
.navigation {
|
.navigation {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrump {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventree-body {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventree-pre-content {
|
||||||
|
width: 100%;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
.inventree-content {
|
.inventree-content {
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
padding-top: 15px;
|
padding-top: 5px;
|
||||||
margin-right: 50px;
|
width: auto;
|
||||||
margin-left: 50px;
|
|
||||||
width: 100%;
|
|
||||||
transition: 0.1s;
|
transition: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +186,7 @@
|
|||||||
position: fixed; /* Stay in place */
|
position: fixed; /* Stay in place */
|
||||||
background-color: #fff; /* Black*/
|
background-color: #fff; /* Black*/
|
||||||
overflow-x: hidden; /* Disable horizontal scroll */
|
overflow-x: hidden; /* Disable horizontal scroll */
|
||||||
//transition: 0.1s; /* 0.5 second transition effect to slide in the sidenav */
|
transition: 0.1s; /* 0.5 second transition effect to slide in the sidenav */
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
|
@ -1,4 +1,26 @@
|
|||||||
function loadTree(url, tree, data) {
|
function loadTree(url, tree, options={}) {
|
||||||
|
/* Load the side-nav tree view
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: URL to request tree data
|
||||||
|
tree: html ref to treeview
|
||||||
|
options:
|
||||||
|
data: data object to pass to the AJAX request
|
||||||
|
selected: ID of currently selected item
|
||||||
|
name: name of the tree
|
||||||
|
*/
|
||||||
|
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
if (options.data) {
|
||||||
|
data = options.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = "inventree-sidenav-items-";
|
||||||
|
|
||||||
|
if (options.name) {
|
||||||
|
key += options.name;
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
@ -9,14 +31,17 @@ function loadTree(url, tree, data) {
|
|||||||
if (response.tree) {
|
if (response.tree) {
|
||||||
$(tree).treeview({
|
$(tree).treeview({
|
||||||
data: response.tree,
|
data: response.tree,
|
||||||
enableLinks: true
|
enableLinks: true,
|
||||||
|
showTags: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
var saved_exp = sessionStorage.getItem('inventree-sidenav-expanded-items').split(",");
|
if (sessionStorage.getItem(key)) {
|
||||||
|
var saved_exp = sessionStorage.getItem(key).split(",");
|
||||||
|
|
||||||
// Automatically expand the desired notes
|
// Automatically expand the desired notes
|
||||||
for (var q = 0; q < saved_exp.length; q++) {
|
for (var q = 0; q < saved_exp.length; q++) {
|
||||||
$(tree).treeview('expandNode', parseInt(saved_exp[q]));
|
$(tree).treeview('expandNode', parseInt(saved_exp[q]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup a callback whenever a node is toggled
|
// Setup a callback whenever a node is toggled
|
||||||
@ -32,7 +57,7 @@ function loadTree(url, tree, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the expanded nodes
|
// Save the expanded nodes
|
||||||
sessionStorage.setItem('inventree-sidenav-expanded-items', exp);
|
sessionStorage.setItem(key, exp);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -43,6 +68,7 @@ function loadTree(url, tree, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openSideNav() {
|
function openSideNav() {
|
||||||
|
document.getElementById("sidenav").style.display = "block";
|
||||||
document.getElementById("sidenav").style.width = "250px";
|
document.getElementById("sidenav").style.width = "250px";
|
||||||
document.getElementById("inventree-content").style.marginLeft = "270px";
|
document.getElementById("inventree-content").style.marginLeft = "270px";
|
||||||
|
|
||||||
@ -52,8 +78,9 @@ function openSideNav() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeSideNav() {
|
function closeSideNav() {
|
||||||
|
document.getElementById("sidenav").style.display = "none";
|
||||||
document.getElementById("sidenav").style.width = "0";
|
document.getElementById("sidenav").style.width = "0";
|
||||||
document.getElementById("inventree-content").style.marginLeft = "50px";
|
document.getElementById("inventree-content").style.marginLeft = "0px";
|
||||||
|
|
||||||
sessionStorage.setItem('inventree-sidenav-state', 'closed');
|
sessionStorage.setItem('inventree-sidenav-state', 'closed');
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ from django_filters.rest_framework import FilterSet, DjangoFilterBackend
|
|||||||
from django_filters import NumberFilter
|
from django_filters import NumberFilter
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from .models import StockLocation, StockItem
|
from .models import StockLocation, StockItem
|
||||||
@ -27,6 +28,10 @@ class StockCategoryTree(TreeSerializer):
|
|||||||
title = 'Stock'
|
title = 'Stock'
|
||||||
model = StockLocation
|
model = StockLocation
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_url(self):
|
||||||
|
return reverse('stock-index')
|
||||||
|
|
||||||
|
|
||||||
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
""" API detail endpoint for Stock object
|
""" API detail endpoint for Stock object
|
||||||
|
@ -48,6 +48,18 @@ class StockLocation(InvenTreeTree):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stock_item_count(self):
|
||||||
|
""" Return the number of StockItem objects which live in or under this category
|
||||||
|
"""
|
||||||
|
|
||||||
|
return len(StockItem.objects.filter(location__in=self.getUniqueChildren()))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_count(self):
|
||||||
|
|
||||||
|
return self.stock_item_count
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log')
|
@receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log')
|
||||||
def before_delete_stock_location(sender, instance, using, **kwargs):
|
def before_delete_stock_location(sender, instance, using, **kwargs):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href='#' id='toggle-stock-tree'><b>+</b></a></li>
|
<li><a href='#' title='Toggle Stock Tree' id='toggle-stock-tree'><b><span class='glyphicon glyphicon-small glyphicon-th-list'></span></b></a></li>
|
||||||
<li class="breadcrumb-item{% if location is None %} active" aria-current="page{% endif %}"><a href="/stock/">Stock</a></li>
|
<li class="breadcrumb-item{% if location is None %} active" aria-current="page{% endif %}"><a href="/stock/">Stock</a></li>
|
||||||
{% if location %}
|
{% if location %}
|
||||||
{% for path_item in location.parentpath %}
|
{% for path_item in location.parentpath %}
|
||||||
|
@ -33,7 +33,12 @@ InvenTree | Stock
|
|||||||
initSideNav();
|
initSideNav();
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
loadTree("{% url 'api-stock-tree' %}",
|
loadTree("{% url 'api-stock-tree' %}",
|
||||||
"#stock-tree");
|
"#stock-tree",
|
||||||
|
{
|
||||||
|
name: 'stock',
|
||||||
|
selected: 'elab',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
$("#toggle-stock-tree").click(function() {
|
$("#toggle-stock-tree").click(function() {
|
||||||
toggleSideNav("#sidenav");
|
toggleSideNav("#sidenav");
|
||||||
|
@ -143,9 +143,13 @@ class StockItemEdit(AjaxUpdateView):
|
|||||||
|
|
||||||
item = self.get_object()
|
item = self.get_object()
|
||||||
|
|
||||||
query = form.fields['supplier_part'].queryset
|
# If the part cannot be purchased, hide the supplier_part field
|
||||||
query = query.filter(part=item.part.id)
|
if not item.part.purchaseable:
|
||||||
form.fields['supplier_part'].queryset = query
|
form.fields['supplier_part'].widget = HiddenInput()
|
||||||
|
else:
|
||||||
|
query = form.fields['supplier_part'].queryset
|
||||||
|
query = query.filter(part=item.part.id)
|
||||||
|
form.fields['supplier_part'].queryset = query
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
@ -206,6 +210,11 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
part = Part.objects.get(id=part_id)
|
part = Part.objects.get(id=part_id)
|
||||||
parts = form.fields['supplier_part'].queryset
|
parts = form.fields['supplier_part'].queryset
|
||||||
parts = parts.filter(part=part.id)
|
parts = parts.filter(part=part.id)
|
||||||
|
|
||||||
|
# If the part is NOT purchaseable, hide the supplier_part field
|
||||||
|
if not part.purchaseable:
|
||||||
|
form.fields['supplier_part'].widget = HiddenInput()
|
||||||
|
|
||||||
form.fields['supplier_part'].queryset = parts
|
form.fields['supplier_part'].queryset = parts
|
||||||
|
|
||||||
# If there is one (and only one) supplier part available, pre-select it
|
# If there is one (and only one) supplier part available, pre-select it
|
||||||
|
@ -52,19 +52,26 @@ InvenTree
|
|||||||
|
|
||||||
<div class='main body wrapper'>
|
<div class='main body wrapper'>
|
||||||
|
|
||||||
|
<div class='inventree-body'>
|
||||||
|
|
||||||
|
<div class='containter-fluid inventree-pre-content'>
|
||||||
|
{% block pre_content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='sidenav' id='sidenav'>
|
<div class='sidenav' id='sidenav'>
|
||||||
{% block sidenav %}
|
{% block sidenav %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container container-fluid inventree-content" id='inventree-content'>
|
<div class="container container-fluid inventree-content" id='inventree-content'>
|
||||||
{% block pre_content %}
|
|
||||||
{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Each view fills in here.. -->
|
<!-- Each view fills in here.. -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block post_content %}
|
{% block post_content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% include 'modals.html' %}
|
||||||
@ -72,6 +79,8 @@ InvenTree
|
|||||||
{% include 'notification.html' %}
|
{% include 'notification.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
|
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/jquery.form.min.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/jquery.form.min.js' %}"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user