Merge branch 'inventree:master' into webhooks

This commit is contained in:
Matthias Mair 2021-12-11 00:08:56 +01:00 committed by GitHub
commit f6d43946b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 25517 additions and 17226 deletions

View File

@ -215,7 +215,7 @@
}
.treeview .list-group-item {
padding: 10px 5px;
padding: 3px 5px;
}
.treeview .list-group-item .indent {
@ -539,7 +539,7 @@
padding: 0px 10px;
}
.breadcrump {
.breadcrumb {
margin-bottom: 5px;
margin-left: 5px;
margin-right: 10px;

View File

@ -1150,7 +1150,7 @@ class BuildItem(models.Model):
bom_item_valid = False
if self.bom_item:
if self.bom_item and self.build:
"""
A BomItem object has already been assigned. This is valid if:
@ -1162,8 +1162,11 @@ class BuildItem(models.Model):
iii) The Part referenced by the StockItem is a valid substitute for the BomItem
"""
if self.build and self.build.part == self.bom_item.part:
if self.build.part == self.bom_item.part:
bom_item_valid = self.bom_item.is_stock_item_valid(self.stock_item)
elif self.bom_item.inherited:
if self.build.part in self.bom_item.part.get_descendants(include_self=False):
bom_item_valid = self.bom_item.is_stock_item_valid(self.stock_item)
# If the existing BomItem is *not* valid, try to find a match

View File

@ -309,13 +309,19 @@ class BuildAllocationItemSerializer(serializers.Serializer):
)
def validate_bom_item(self, bom_item):
# TODO: Fix this validation - allow for variants and substitutes!
"""
Check if the parts match!
"""
build = self.context['build']
# BomItem must point to the same 'part' as the parent build
# BomItem should point to the same 'part' as the parent build
if build.part != bom_item.part:
# If not, it may be marked as "inherited" from a parent part
if bom_item.inherited and build.part in bom_item.part.get_descendants(include_self=False):
pass
else:
raise ValidationError(_("bom_item.part must point to the same part as the build order"))
return bom_item

View File

@ -77,6 +77,11 @@ src="{% static 'img/blank_image.png' %}"
<td>{% trans "Part" %}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
</tr>
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>
<td>{{ build.quantity }}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Build Description" %}</td>
@ -127,11 +132,6 @@ src="{% static 'img/blank_image.png' %}"
{% block details_right %}
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>
<td>{{ build.quantity }}</td>
</tr>
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -239,6 +239,23 @@ class CategoryParameterList(generics.ListAPIView):
return queryset
class CategoryTree(generics.ListAPIView):
"""
API endpoint for accessing a list of PartCategory objects ready for rendering a tree.
"""
queryset = PartCategory.objects.all()
serializer_class = part_serializers.CategoryTree
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
]
# Order by tree level (top levels first) and then name
ordering = ['level', 'name']
class PartSalePriceList(generics.ListCreateAPIView):
"""
API endpoint for list view of PartSalePriceBreak model
@ -1515,6 +1532,7 @@ part_api_urls = [
# Base URL for PartCategory API endpoints
url(r'^category/', include([
url(r'^tree/', CategoryTree.as_view(), name='api-part-category-tree'),
url(r'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),

View File

@ -70,6 +70,20 @@ class CategorySerializer(InvenTreeModelSerializer):
]
class CategoryTree(InvenTreeModelSerializer):
"""
Serializer for PartCategory tree
"""
class Meta:
model = PartCategory
fields = [
'pk',
'name',
'parent',
]
class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializer for the PartAttachment class

View File

@ -6,6 +6,10 @@
{% include 'part/category_sidebar.html' %}
{% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block heading %}
{% if category %}
{% trans "Part Category" %}: {{ category.name }}
@ -239,8 +243,24 @@
{% endif %}
// Enable left-hand navigation sidebar
enableSidebar('category');
// Enable breadcrumb tree view
enableBreadcrumbTree({
label: 'category',
url: '{% url "api-part-category-tree" %}',
{% if category %}
selected: {{ category.pk }},
{% endif %}
processNode: function(node) {
node.text = node.name;
node.href = `/part/category/${node.pk}/`;
return node;
}
});
loadPartCategoryTable(
$('#subcategory-table'), {
params: {

View File

@ -9,6 +9,10 @@
{% include 'part/part_sidebar.html' %}
{% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block page_content %}
<div class='panel panel-hidden' id='panel-part-stock'>
@ -132,10 +136,6 @@
</div>
</div>
<div class='panel panel-hidden' id='panel-pricing'>
<!-- TODO -->
</div>
<div class='panel panel-hidden' id='panel-variants'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
@ -1066,4 +1066,18 @@
enableSidebar('part');
enableBreadcrumbTree({
label: 'part',
url: '{% url "api-part-category-tree" %}',
{% if part.category %}
selected: {{ part.category.pk }},
{% endif %}
processNode: function(node) {
node.text = node.name;
node.href = `/part/category/${node.pk}/`;
return node;
}
});
{% endblock %}

View File

@ -14,9 +14,10 @@
{% endblock %}
{% block breadcrumbs %}
<a href='#' id='breadcrumb-tree-toggle' class="breadcrumb-item"><i class="fas fa-bars"></i></a>
{% if part %}
{% include "part/cat_link.html" with category=part.category part=part %}
{% else %}
{% include 'part/cat_link.html' with category=category %}
{% endif %}
{% endblock %}
{% endblock breadcrumbs %}

View File

@ -277,6 +277,24 @@ class StockLocationList(generics.ListCreateAPIView):
]
class StockLocationTree(generics.ListAPIView):
"""
API endpoint for accessing a list of StockLocation objects,
ready for rendering as a tree
"""
queryset = StockLocation.objects.all()
serializer_class = StockSerializers.LocationTreeSerializer
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
]
# Order by tree level (top levels first) and then name
ordering = ['level', 'name']
class StockFilter(rest_filters.FilterSet):
"""
FilterSet for StockItem LIST API
@ -1182,6 +1200,9 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
stock_api_urls = [
url(r'^location/', include([
url(r'^tree/', StockLocationTree.as_view(), name='api-location-tree'),
url(r'^(?P<pk>\d+)/', LocationDetail.as_view(), name='api-location-detail'),
url(r'^.*$', StockLocationList.as_view(), name='api-location-list'),
])),

View File

@ -390,6 +390,20 @@ class SerializeStockItemSerializer(serializers.Serializer):
)
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""
Serializer for a simple tree view
"""
class Meta:
model = StockLocation
fields = [
'pk',
'name',
'parent',
]
class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Detailed information about a stock location
"""

View File

@ -9,9 +9,15 @@
{% endblock %}
{% block breadcrumbs %}
<a href='#' id='breadcrumb-tree-toggle' class="breadcrumb-item"><i class="fas fa-bars"></i></a>
{% include 'stock/loc_link.html' with location=item.location %}
{% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block heading %}
{% trans "Stock Item" %}: {{ item.part.full_name}}
{% endblock heading %}
@ -611,4 +617,18 @@ $('#serial-number-search').click(function() {
findStockItemBySerialNumber({{ item.part.pk }});
});
enableBreadcrumbTree({
label: 'stockitem',
url: '{% url "api-location-tree" %}',
{% if item.location %}
selected: {{ item.location.pk }},
{% endif %}
processNode: function(node) {
node.text = node.name;
node.href = `/stock/item/${node.pk}/`;
return node;
}
});
{% endblock %}

View File

@ -7,6 +7,10 @@
{% include "stock/location_sidebar.html" %}
{% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block heading %}
{% if location %}
{% trans "Stock Location" %}: {{ location.name }}
@ -348,4 +352,19 @@
enableSidebar('stocklocation');
// Enable breadcrumb tree view
enableBreadcrumbTree({
label: 'location',
url: '{% url "api-location-tree" %}',
{% if location %}
selected: {{ location.pk }},
{% endif %}
processNode: function(node) {
node.text = node.name;
node.href = `/stock/location/${node.pk}/`;
return node;
}
});
{% endblock %}

View File

@ -18,9 +18,10 @@
{% endblock %}
{% block breadcrumbs %}
<a href='#' id='breadcrumb-tree-toggle' class="breadcrumb-item"><i class="fas fa-bars"></i></a>
{% if item %}
{% include 'stock/loc_link.html' with location=item.location %}
{% else %}
{% include 'stock/loc_link.html' with location=location %}
{% endif %}
{% endblock %}
{% endblock breadcrumbs %}

View File

@ -74,13 +74,13 @@
<div class='row flex-nowrap inventree-body'>
<div class='col-auto px-1 sidebar-wrapper'>
<div id='sidebar' class='collapse collapse-horizontal show border-end' style='display: none;'>
<div id='sidebar' class='collapse collapse-horizontal show' style='display: none;'>
<div id='sidebar-nav' class='list-group text-sm-start'>
<ul id='sidebar-list-group' class='list-group sidebar-list-group'>
{% block sidebar %}
<!-- Sidebar goes here -->
{% endblock %}
{% include "sidebar_toggle.html" %}
{% include "sidebar_toggle.html" with target='sidebar' %}
</ul>
</div>
</div>
@ -104,14 +104,20 @@
{% endblock %}
{% block breadcrumb_list %}
<div class='container-fluid navigation'>
<div class='container-fluid navigation' id='breadcrumb-div'>
<nav aria-label='breadcrumb'>
<ol class='breadcrumb'>
<ol class='breadcrumb' id='breadcrumb-list'>
{% block breadcrumbs %}
{% endblock %}
</ol>
</nav>
<div id='breadcrumb-tree-collapse' class='collapse collapse-horizontal show border' style='display: none;'>
{% block breadcrumb_tree %}
{% endblock %}
</div>
</div>
{% endblock %}
{% block content %}

View File

@ -6,6 +6,7 @@
addSidebarHeader,
addSidebarItem,
addSidebarLink,
enableBreadcrumbTree,
enableSidebar,
onPanelLoad,
*/
@ -145,6 +146,101 @@ function enableSidebar(label, options={}) {
}
/**
* Enable support for breadcrumb tree navigation on this page
*/
function enableBreadcrumbTree(options) {
var label = options.label;
if (!label) {
console.log('ERROR: enableBreadcrumbTree called without supplying label');
return;
}
var filters = options.filters || {};
inventreeGet(
options.url,
filters,
{
success: function(data) {
// Data are returned from the InvenTree server as a flattened list;
// We need to convert this into a tree structure
var nodes = {};
var roots = [];
var node = null;
for (var i = 0; i < data.length; i++) {
node = data[i];
node.nodes = [];
nodes[node.pk] = node;
node.selectable = false;
if (options.processNode) {
node = options.processNode(node);
}
node.state = {
expanded: node.pk == options.selected,
selected: node.pk == options.selected,
};
}
for (var i = 0; i < data.length; i++) {
node = data[i];
if (node.parent != null) {
nodes[node.parent].nodes.push(node);
if (node.state.expanded) {
nodes[node.parent].state.expanded = true;
}
} else {
roots.push(node);
}
}
$('#breadcrumb-tree').treeview({
data: roots,
showTags: true,
enableLinks: true,
expandIcon: 'fas fa-chevron-right',
collapseIcon: 'fa fa-chevron-down',
});
setBreadcrumbTreeState(label, state);
}
}
);
$('#breadcrumb-tree-toggle').click(function() {
// Add callback to "collapse" and "expand" the sidebar
// By default, the menu is "expanded"
var state = localStorage.getItem(`inventree-tree-state-${label}`) || 'expanded';
// We wish to "toggle" the state!
setBreadcrumbTreeState(label, state == 'expanded' ? 'collapsed' : 'expanded');
});
// Set the initial state (default = expanded)
var state = localStorage.getItem(`inventree-tree-state-${label}`) || 'expanded';
function setBreadcrumbTreeState(label, state) {
if (state == 'collapsed') {
$('#breadcrumb-tree-collapse').hide(100);
} else {
$('#breadcrumb-tree-collapse').show(100);
}
localStorage.setItem(`inventree-tree-state-${label}`, state);
}
}
/*
* Set the "toggle" state of the sidebar
@ -180,7 +276,7 @@ function setSidebarState(label, state) {
function addSidebarItem(options={}) {
var html = `
<a href='#' id='select-${options.label}' title='${options.text}' class='list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate sidebar-selector' data-bs-parent='#sidebar'>
<a href='#' id='select-${options.label}' title='${options.text}' class='list-group-item sidebar-list-group-item border-end d-inline-block text-truncate sidebar-selector' data-bs-parent='#sidebar'>
<i class='bi bi-bootstrap'></i>
${options.content_before || ''}
<span class='sidebar-item-icon fas ${options.icon}'></span>
@ -199,7 +295,7 @@ function addSidebarItem(options={}) {
function addSidebarHeader(options={}) {
var html = `
<span title='${options.text}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate" data-bs-parent="#sidebar">
<span title='${options.text}' class="list-group-item sidebar-list-group-item border-end d-inline-block text-truncate" data-bs-parent="#sidebar">
<h6>
<i class="bi bi-bootstrap"></i>
<span class='sidebar-item-text' style='display: none;'>${options.text}</span>

View File

@ -1658,7 +1658,7 @@ function allocateStockToSalesOrder(order_id, line_items, options={}) {
var available = Math.max((data.quantity || 0) - (data.allocated || 0), 0);
// Remaining quantity to be allocated?
var remaining = opts.quantity || available;
var remaining = Math.max(line_item.quantity - line_item.shipped - line_item.allocated, 0);
// Maximum amount that we need
var desired = Math.min(available, remaining);

View File

@ -1,5 +1,5 @@
{% load i18n %}
<span title='{{ text }}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate bg-light" data-bs-parent="#sidebar">
<span title='{{ text }}' class="list-group-item sidebar-list-group-item border-end d-inline-block text-truncate bg-light" data-bs-parent="#sidebar">
<h6>
<i class="bi bi-bootstrap"></i>
{% if icon %}<span class='sidebar-item-icon fas {{ icon }}'></span>{% endif %}

View File

@ -1,5 +1,5 @@
{% load i18n %}
<a href="#" id='select-{{ label }}' title='{{ text }}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate sidebar-selector" data-bs-parent="#sidebar">
<a href="#" id='select-{{ label }}' title='{{ text }}' class="list-group-item sidebar-list-group-item border-end d-inline-block text-truncate sidebar-selector" data-bs-parent="#sidebar">
<i class="bi bi-bootstrap"></i>
<span class='sidebar-item-icon fas {{ icon|default:"fa-circle" }}'></span>
<span class='sidebar-item-text' style='display: none;'>{{ text }}</span>

View File

@ -1,4 +1,4 @@
{% load i18n %}
<a href="{{ url }}" class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate" data-bs-parent="#sidebar">
<a href="{{ url }}" class="list-group-item sidebar-list-group-item border-end d-inline-block text-truncate" data-bs-parent="#sidebar">
<i class="bi bi-bootstrap"></i><span class='sidebar-item-icon fas {{ icon }}'></span><span class='sidebar-item-text' style='display: none;'>{{ text }}</span>
</a>

View File

@ -1,3 +1,4 @@
<a href="#" id='sidebar-toggle' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate sidebar-toggle" data-bs-parent="#sidebar" style='display: none;'>
<a href="#" id='{{ target }}-toggle' class="list-group-item sidebar-list-group-item border-end d-inline-block text-truncate sidebar-toggle" data-bs-parent="#sidebar" style='display: none;'>
<i class="bi bi-bootstrap"></i><span id='sidebar-toggle-icon' class='sidebar-item-icon fas fa-chevron-left'></span>
{% if text %}<span class='sidebar-item-text' style='display: none;'>{{ text }}</span>{% endif %}
</a>