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

View File

@ -1150,7 +1150,7 @@ class BuildItem(models.Model):
bom_item_valid = False 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: 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 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) 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 # 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): 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'] 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 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")) raise ValidationError(_("bom_item.part must point to the same part as the build order"))
return bom_item return bom_item

View File

@ -77,6 +77,11 @@ src="{% static 'img/blank_image.png' %}"
<td>{% trans "Part" %}</td> <td>{% trans "Part" %}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td> <td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
</tr> </tr>
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>
<td>{{ build.quantity }}</td>
</tr>
<tr> <tr>
<td><span class='fas fa-info-circle'></span></td> <td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Build Description" %}</td> <td>{% trans "Build Description" %}</td>
@ -127,11 +132,6 @@ src="{% static 'img/blank_image.png' %}"
{% block details_right %} {% block details_right %}
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<col width='25'> <col width='25'>
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>
<td>{{ build.quantity }}</td>
</tr>
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</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 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): class PartSalePriceList(generics.ListCreateAPIView):
""" """
API endpoint for list view of PartSalePriceBreak model API endpoint for list view of PartSalePriceBreak model
@ -1515,6 +1532,7 @@ part_api_urls = [
# Base URL for PartCategory API endpoints # Base URL for PartCategory API endpoints
url(r'^category/', include([ 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'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'), 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): class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """
Serializer for the PartAttachment class Serializer for the PartAttachment class

View File

@ -6,6 +6,10 @@
{% include 'part/category_sidebar.html' %} {% include 'part/category_sidebar.html' %}
{% endblock %} {% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block heading %} {% block heading %}
{% if category %} {% if category %}
{% trans "Part Category" %}: {{ category.name }} {% trans "Part Category" %}: {{ category.name }}
@ -239,8 +243,24 @@
{% endif %} {% endif %}
// Enable left-hand navigation sidebar
enableSidebar('category'); 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( loadPartCategoryTable(
$('#subcategory-table'), { $('#subcategory-table'), {
params: { params: {

View File

@ -9,6 +9,10 @@
{% include 'part/part_sidebar.html' %} {% include 'part/part_sidebar.html' %}
{% endblock %} {% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block page_content %} {% block page_content %}
<div class='panel panel-hidden' id='panel-part-stock'> <div class='panel panel-hidden' id='panel-part-stock'>
@ -132,10 +136,6 @@
</div> </div>
</div> </div>
<div class='panel panel-hidden' id='panel-pricing'>
<!-- TODO -->
</div>
<div class='panel panel-hidden' id='panel-variants'> <div class='panel panel-hidden' id='panel-variants'>
<div class='panel-heading'> <div class='panel-heading'>
<div class='d-flex flex-wrap'> <div class='d-flex flex-wrap'>
@ -1066,4 +1066,18 @@
enableSidebar('part'); 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 %} {% endblock %}

View File

@ -14,9 +14,10 @@
{% endblock %} {% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<a href='#' id='breadcrumb-tree-toggle' class="breadcrumb-item"><i class="fas fa-bars"></i></a>
{% if part %} {% if part %}
{% include "part/cat_link.html" with category=part.category part=part %} {% include "part/cat_link.html" with category=part.category part=part %}
{% else %} {% else %}
{% include 'part/cat_link.html' with category=category %} {% include 'part/cat_link.html' with category=category %}
{% endif %} {% 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): class StockFilter(rest_filters.FilterSet):
""" """
FilterSet for StockItem LIST API FilterSet for StockItem LIST API
@ -1182,6 +1200,9 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
stock_api_urls = [ stock_api_urls = [
url(r'^location/', include([ 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'^(?P<pk>\d+)/', LocationDetail.as_view(), name='api-location-detail'),
url(r'^.*$', StockLocationList.as_view(), name='api-location-list'), 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): class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Detailed information about a stock location """ Detailed information about a stock location
""" """

View File

@ -9,9 +9,15 @@
{% endblock %} {% endblock %}
{% block breadcrumbs %} {% 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 %} {% include 'stock/loc_link.html' with location=item.location %}
{% endblock %} {% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block heading %} {% block heading %}
{% trans "Stock Item" %}: {{ item.part.full_name}} {% trans "Stock Item" %}: {{ item.part.full_name}}
{% endblock heading %} {% endblock heading %}
@ -611,4 +617,18 @@ $('#serial-number-search').click(function() {
findStockItemBySerialNumber({{ item.part.pk }}); 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 %} {% endblock %}

View File

@ -7,6 +7,10 @@
{% include "stock/location_sidebar.html" %} {% include "stock/location_sidebar.html" %}
{% endblock %} {% endblock %}
{% block breadcrumb_tree %}
<div id="breadcrumb-tree"></div>
{% endblock breadcrumb_tree %}
{% block heading %} {% block heading %}
{% if location %} {% if location %}
{% trans "Stock Location" %}: {{ location.name }} {% trans "Stock Location" %}: {{ location.name }}
@ -348,4 +352,19 @@
enableSidebar('stocklocation'); 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 %} {% endblock %}

View File

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

View File

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

View File

@ -6,6 +6,7 @@
addSidebarHeader, addSidebarHeader,
addSidebarItem, addSidebarItem,
addSidebarLink, addSidebarLink,
enableBreadcrumbTree,
enableSidebar, enableSidebar,
onPanelLoad, 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 * Set the "toggle" state of the sidebar
@ -180,7 +276,7 @@ function setSidebarState(label, state) {
function addSidebarItem(options={}) { function addSidebarItem(options={}) {
var html = ` 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> <i class='bi bi-bootstrap'></i>
${options.content_before || ''} ${options.content_before || ''}
<span class='sidebar-item-icon fas ${options.icon}'></span> <span class='sidebar-item-icon fas ${options.icon}'></span>
@ -199,7 +295,7 @@ function addSidebarItem(options={}) {
function addSidebarHeader(options={}) { function addSidebarHeader(options={}) {
var html = ` 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> <h6>
<i class="bi bi-bootstrap"></i> <i class="bi bi-bootstrap"></i>
<span class='sidebar-item-text' style='display: none;'>${options.text}</span> <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); var available = Math.max((data.quantity || 0) - (data.allocated || 0), 0);
// Remaining quantity to be allocated? // 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 // Maximum amount that we need
var desired = Math.min(available, remaining); var desired = Math.min(available, remaining);

View File

@ -1,5 +1,5 @@
{% load i18n %} {% 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> <h6>
<i class="bi bi-bootstrap"></i> <i class="bi bi-bootstrap"></i>
{% if icon %}<span class='sidebar-item-icon fas {{ icon }}'></span>{% endif %} {% if icon %}<span class='sidebar-item-icon fas {{ icon }}'></span>{% endif %}

View File

@ -1,5 +1,5 @@
{% load i18n %} {% 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> <i class="bi bi-bootstrap"></i>
<span class='sidebar-item-icon fas {{ icon|default:"fa-circle" }}'></span> <span class='sidebar-item-icon fas {{ icon|default:"fa-circle" }}'></span>
<span class='sidebar-item-text' style='display: none;'>{{ text }}</span> <span class='sidebar-item-text' style='display: none;'>{{ text }}</span>

View File

@ -1,4 +1,4 @@
{% load i18n %} {% 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> <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> </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> <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> </a>