Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-09 23:07:42 +10:00
commit 9b0825739a
14 changed files with 154 additions and 29 deletions

View File

@ -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

View File

@ -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 = {

View File

@ -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.

View File

@ -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):

View File

@ -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 %}

View File

@ -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");

View File

@ -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 {

View File

@ -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,15 +31,18 @@ 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
$(tree).on('nodeExpanded nodeCollapsed', function(event, data) { $(tree).on('nodeExpanded nodeCollapsed', function(event, data) {
@ -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');
} }

View File

@ -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

View File

@ -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):

View File

@ -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 %}

View File

@ -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");

View File

@ -143,6 +143,10 @@ class StockItemEdit(AjaxUpdateView):
item = self.get_object() item = self.get_object()
# If the part cannot be purchased, hide the supplier_part field
if not item.part.purchaseable:
form.fields['supplier_part'].widget = HiddenInput()
else:
query = form.fields['supplier_part'].queryset query = form.fields['supplier_part'].queryset
query = query.filter(part=item.part.id) query = query.filter(part=item.part.id)
form.fields['supplier_part'].queryset = query form.fields['supplier_part'].queryset = query
@ -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

View File

@ -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>