mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2694
This commit is contained in:
commit
4ab27e5f3c
@ -1018,4 +1018,33 @@ input[type='number']{
|
|||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quicksearch Panel */
|
||||||
|
|
||||||
|
.search-result-panel {
|
||||||
|
max-width: 800px;
|
||||||
|
width: 75%
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-group {
|
||||||
|
padding: 5px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-group-buttons > button{
|
||||||
|
padding: 2px;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-entry {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 3px;
|
||||||
|
margin-top: 3px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
@ -128,81 +128,6 @@ function inventreeDocReady() {
|
|||||||
attachClipboard('.clip-btn', 'modal-about');
|
attachClipboard('.clip-btn', 'modal-about');
|
||||||
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text');
|
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text');
|
||||||
|
|
||||||
// Add autocomplete to the search-bar
|
|
||||||
if ($('#search-bar').exists()) {
|
|
||||||
$('#search-bar').autocomplete({
|
|
||||||
source: function(request, response) {
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
search: request.term,
|
|
||||||
limit: user_settings.SEARCH_PREVIEW_RESULTS,
|
|
||||||
offset: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) {
|
|
||||||
// Limit to active parts
|
|
||||||
params.active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/part/',
|
|
||||||
data: params,
|
|
||||||
success: function(data) {
|
|
||||||
|
|
||||||
var transformed = $.map(data.results, function(el) {
|
|
||||||
return {
|
|
||||||
label: el.full_name,
|
|
||||||
id: el.pk,
|
|
||||||
thumbnail: el.thumbnail,
|
|
||||||
data: el,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
response(transformed);
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
response([]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
create: function() {
|
|
||||||
$(this).data('ui-autocomplete')._renderItem = function(ul, item) {
|
|
||||||
|
|
||||||
var html = `
|
|
||||||
<div class='search-autocomplete-item' title='${item.data.description}'>
|
|
||||||
<a href='/part/${item.id}/'>
|
|
||||||
<span style='padding-right: 10px;'><img class='hover-img-thumb' src='${item.thumbnail || "/static/img/blank_image.png"}'> ${item.label}</span>
|
|
||||||
</a>
|
|
||||||
<span class='flex' style='flex-grow: 1;'></span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (user_settings.SEARCH_SHOW_STOCK_LEVELS) {
|
|
||||||
html += partStockLabel(
|
|
||||||
item.data,
|
|
||||||
{
|
|
||||||
classes: 'badge-right',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
return $('<li>').append(html).appendTo(ul);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
select: function( event, ui ) {
|
|
||||||
window.location = '/part/' + ui.item.id + '/';
|
|
||||||
},
|
|
||||||
minLength: 2,
|
|
||||||
classes: {
|
|
||||||
'ui-autocomplete': 'dropdown-menu search-menu',
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
my : "right top",
|
|
||||||
at: "right bottom"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate brand-icons
|
// Generate brand-icons
|
||||||
$('.brand-icon').each(function(i, obj) {
|
$('.brand-icon').each(function(i, obj) {
|
||||||
loadBrandIcon($(this), $(this).attr('brand_name'));
|
loadBrandIcon($(this), $(this).attr('brand_name'));
|
||||||
@ -231,8 +156,13 @@ function inventreeDocReady() {
|
|||||||
stopNotificationWatcher();
|
stopNotificationWatcher();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#offcanvasRight').on('show.bs.offcanvas', openNotificationPanel); // listener for opening the notification panel
|
// Calbacks for search panel
|
||||||
$('#offcanvasRight').on('hidden.bs.offcanvas', closeNotificationPanel); // listener for closing the notification panel
|
$('#offcanvas-search').on('shown.bs.offcanvas', openSearchPanel);
|
||||||
|
$('#offcanvas-search').on('hidden.bs.offcanvas', closeSearchPanel);
|
||||||
|
|
||||||
|
// Callbacks for notifications panel
|
||||||
|
$('#offcanvas-notification').on('show.bs.offcanvas', openNotificationPanel); // listener for opening the notification panel
|
||||||
|
$('#offcanvas-notification').on('hidden.bs.offcanvas', closeNotificationPanel); // listener for closing the notification panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ class ViewTests(TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Change this number as more javascript files are added to the index page
|
# Change this number as more javascript files are added to the index page
|
||||||
N_SCRIPT_FILES = 37
|
N_SCRIPT_FILES = 38
|
||||||
|
|
||||||
content = self.get_index_page()
|
content = self.get_index_page()
|
||||||
|
|
||||||
|
@ -130,6 +130,7 @@ translated_javascript_urls = [
|
|||||||
url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
|
url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
|
||||||
url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
|
url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
|
||||||
url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
|
url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
|
||||||
|
url(r'^search.js', DynamicJsView.as_view(template_name='js/translated/search.js'), name='search.js'),
|
||||||
url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
|
url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
|
||||||
url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'),
|
url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'),
|
||||||
url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
|
url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
|
||||||
|
@ -12,11 +12,18 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 34
|
INVENTREE_API_VERSION = 36
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v36 -> 2022-04-03
|
||||||
|
- Adds ability to filter part list endpoint by unallocated_stock argument
|
||||||
|
|
||||||
|
v35 -> 2022-04-01 : https://github.com/inventree/InvenTree/pull/2797
|
||||||
|
- Adds stock allocation information to the Part API
|
||||||
|
- Adds calculated field for "unallocated_quantity"
|
||||||
|
|
||||||
v34 -> 2022-03-25
|
v34 -> 2022-03-25
|
||||||
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
|
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
|
||||||
|
|
||||||
@ -196,7 +203,7 @@ def isInvenTreeUpToDate():
|
|||||||
and stores it to the database as INVENTREE_LATEST_VERSION
|
and stores it to the database as INVENTREE_LATEST_VERSION
|
||||||
"""
|
"""
|
||||||
|
|
||||||
latest = common.models.InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION', None)
|
latest = common.models.InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION', backup_value=None, create=False)
|
||||||
|
|
||||||
# No record for "latest" version - we must assume we are up to date!
|
# No record for "latest" version - we must assume we are up to date!
|
||||||
if not latest:
|
if not latest:
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{% extends "modal_delete_form.html" %}
|
{% extends "modal_delete_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
Are you sure you want to delete this build?
|
|
||||||
|
{% trans "Are you sure you want to delete this build?" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1247,6 +1247,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'LABEL_ENABLE': {
|
||||||
|
'name': _('Enable label printing'),
|
||||||
|
'description': _('Enable label printing from the web interface'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
"LABEL_INLINE": {
|
"LABEL_INLINE": {
|
||||||
'name': _('Inline label display'),
|
'name': _('Inline label display'),
|
||||||
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
|
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
|
||||||
@ -1261,20 +1268,62 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
'SEARCH_PREVIEW_RESULTS': {
|
'SEARCH_PREVIEW_SHOW_PARTS': {
|
||||||
'name': _('Search Preview Results'),
|
'name': _('Search Parts'),
|
||||||
'description': _('Number of results to show in search preview window'),
|
'description': _('Display parts in search preview window'),
|
||||||
'default': 10,
|
|
||||||
'validator': [int, MinValueValidator(1)]
|
|
||||||
},
|
|
||||||
|
|
||||||
'SEARCH_SHOW_STOCK_LEVELS': {
|
|
||||||
'name': _('Search Show Stock'),
|
|
||||||
'description': _('Display stock levels in search preview window'),
|
|
||||||
'default': True,
|
'default': True,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'SEARCH_PREVIEW_SHOW_CATEGORIES': {
|
||||||
|
'name': _('Search Categories'),
|
||||||
|
'description': _('Display part categories in search preview window'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
'SEARCH_PREVIEW_SHOW_STOCK': {
|
||||||
|
'name': _('Search Stock'),
|
||||||
|
'description': _('Display stock items in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
'SEARCH_PREVIEW_SHOW_LOCATIONS': {
|
||||||
|
'name': _('Search Locations'),
|
||||||
|
'description': _('Display stock locations in search preview window'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
'SEARCH_PREVIEW_SHOW_COMPANIES': {
|
||||||
|
'name': _('Search Companies'),
|
||||||
|
'description': _('Display companies in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
'SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS': {
|
||||||
|
'name': _('Search Purchase Orders'),
|
||||||
|
'description': _('Display purchase orders in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
'SEARCH_PREVIEW_SHOW_SALES_ORDERS': {
|
||||||
|
'name': _('Search Sales Orders'),
|
||||||
|
'description': _('Display sales orders in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
'SEARCH_PREVIEW_RESULTS': {
|
||||||
|
'name': _('Search Preview Results'),
|
||||||
|
'description': _('Number of results to show in each section of the search preview window'),
|
||||||
|
'default': 10,
|
||||||
|
'validator': [int, MinValueValidator(1)]
|
||||||
|
},
|
||||||
|
|
||||||
'SEARCH_HIDE_INACTIVE_PARTS': {
|
'SEARCH_HIDE_INACTIVE_PARTS': {
|
||||||
'name': _("Hide Inactive Parts"),
|
'name': _("Hide Inactive Parts"),
|
||||||
'description': _('Hide inactive parts in search preview window'),
|
'description': _('Hide inactive parts in search preview window'),
|
||||||
|
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
File diff suppressed because it is too large
Load Diff
@ -798,6 +798,20 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
# unallocated_stock filter
|
||||||
|
unallocated_stock = rest_filters.BooleanFilter(label='Unallocated stock', method='filter_unallocated_stock')
|
||||||
|
|
||||||
|
def filter_unallocated_stock(self, queryset, name, value):
|
||||||
|
|
||||||
|
value = str2bool(value)
|
||||||
|
|
||||||
|
if value:
|
||||||
|
queryset = queryset.filter(Q(unallocated_stock__gt=0))
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(Q(unallocated_stock__lte=0))
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
is_template = rest_filters.BooleanFilter()
|
is_template = rest_filters.BooleanFilter()
|
||||||
|
|
||||||
assembly = rest_filters.BooleanFilter()
|
assembly = rest_filters.BooleanFilter()
|
||||||
@ -1334,6 +1348,7 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
'creation_date',
|
'creation_date',
|
||||||
'IPN',
|
'IPN',
|
||||||
'in_stock',
|
'in_stock',
|
||||||
|
'unallocated_stock',
|
||||||
'category',
|
'category',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -38,3 +38,11 @@
|
|||||||
part: 1
|
part: 1
|
||||||
sub_part: 5
|
sub_part: 5
|
||||||
quantity: 3
|
quantity: 3
|
||||||
|
|
||||||
|
# Make "Assembly" from "Bob"
|
||||||
|
- model: part.bomitem
|
||||||
|
pk: 6
|
||||||
|
fields:
|
||||||
|
part: 101
|
||||||
|
sub_part: 100
|
||||||
|
quantity: 10
|
||||||
|
@ -108,6 +108,18 @@
|
|||||||
lft: 0
|
lft: 0
|
||||||
rght: 0
|
rght: 0
|
||||||
|
|
||||||
|
- model: part.part
|
||||||
|
pk: 101
|
||||||
|
fields:
|
||||||
|
name: 'Assembly'
|
||||||
|
description: 'A high level assembly'
|
||||||
|
salable: true
|
||||||
|
active: True
|
||||||
|
tree_id: 0
|
||||||
|
level: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
# A 'template' part
|
# A 'template' part
|
||||||
- model: part.part
|
- model: part.part
|
||||||
pk: 10000
|
pk: 10000
|
||||||
|
@ -1345,7 +1345,8 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
|
queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
|
||||||
|
|
||||||
pending = kwargs.get('pending', None)
|
# Default behaviour is to only return *pending* allocations
|
||||||
|
pending = kwargs.get('pending', True)
|
||||||
|
|
||||||
if pending is True:
|
if pending is True:
|
||||||
# Look only for 'open' orders which have not shipped
|
# Look only for 'open' orders which have not shipped
|
||||||
@ -1433,7 +1434,7 @@ class Part(MPTTModel):
|
|||||||
- If this part is a "template" (variants exist) then these are counted too
|
- If this part is a "template" (variants exist) then these are counted too
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.get_stock_count()
|
return self.get_stock_count(include_variants=True)
|
||||||
|
|
||||||
def get_bom_item_filter(self, include_inherited=True):
|
def get_bom_item_filter(self, include_inherited=True):
|
||||||
"""
|
"""
|
||||||
|
@ -7,7 +7,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Q
|
from django.db.models import ExpressionWrapper, F, Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
@ -24,7 +24,10 @@ from InvenTree.serializers import (DataFileUploadSerializer,
|
|||||||
InvenTreeAttachmentSerializer,
|
InvenTreeAttachmentSerializer,
|
||||||
InvenTreeMoneySerializer)
|
InvenTreeMoneySerializer)
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
from InvenTree.status_codes import (BuildStatus,
|
||||||
|
PurchaseOrderStatus,
|
||||||
|
SalesOrderStatus)
|
||||||
|
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from .models import (BomItem, BomItemSubstitute,
|
from .models import (BomItem, BomItemSubstitute,
|
||||||
@ -363,6 +366,51 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Annotate with the number of stock items allocated to sales orders.
|
||||||
|
This annotation is modeled on Part.sales_order_allocations() method:
|
||||||
|
|
||||||
|
- Only look for "open" orders
|
||||||
|
- Stock items have not been "shipped"
|
||||||
|
"""
|
||||||
|
so_allocation_filter = Q(
|
||||||
|
line__order__status__in=SalesOrderStatus.OPEN, # LineItem points to an OPEN order
|
||||||
|
shipment__shipment_date=None, # Allocated item has *not* been shipped out
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
allocated_to_sales_orders=Coalesce(
|
||||||
|
SubquerySum('stock_items__sales_order_allocations__quantity', filter=so_allocation_filter),
|
||||||
|
Decimal(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Annotate with the number of stock items allocated to build orders.
|
||||||
|
This annotation is modeled on Part.build_order_allocations() method
|
||||||
|
"""
|
||||||
|
bo_allocation_filter = Q(
|
||||||
|
build__status__in=BuildStatus.ACTIVE_CODES,
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
allocated_to_build_orders=Coalesce(
|
||||||
|
SubquerySum('stock_items__allocations__quantity', filter=bo_allocation_filter),
|
||||||
|
Decimal(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Annotate with the total 'available stock' quantity
|
||||||
|
# This is the current stock, minus any allocations
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
unallocated_stock=ExpressionWrapper(
|
||||||
|
F('in_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_starred(self, part):
|
def get_starred(self, part):
|
||||||
@ -376,9 +424,12 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
category_detail = CategorySerializer(source='category', many=False, read_only=True)
|
category_detail = CategorySerializer(source='category', many=False, read_only=True)
|
||||||
|
|
||||||
# Calculated fields
|
# Calculated fields
|
||||||
|
allocated_to_build_orders = serializers.FloatField(read_only=True)
|
||||||
|
allocated_to_sales_orders = serializers.FloatField(read_only=True)
|
||||||
|
unallocated_stock = serializers.FloatField(read_only=True)
|
||||||
|
building = serializers.FloatField(read_only=True)
|
||||||
in_stock = serializers.FloatField(read_only=True)
|
in_stock = serializers.FloatField(read_only=True)
|
||||||
ordering = serializers.FloatField(read_only=True)
|
ordering = serializers.FloatField(read_only=True)
|
||||||
building = serializers.FloatField(read_only=True)
|
|
||||||
stock_item_count = serializers.IntegerField(read_only=True)
|
stock_item_count = serializers.IntegerField(read_only=True)
|
||||||
suppliers = serializers.IntegerField(read_only=True)
|
suppliers = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -399,7 +450,8 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
partial = True
|
partial = True
|
||||||
fields = [
|
fields = [
|
||||||
'active',
|
'active',
|
||||||
|
'allocated_to_build_orders',
|
||||||
|
'allocated_to_sales_orders',
|
||||||
'assembly',
|
'assembly',
|
||||||
'category',
|
'category',
|
||||||
'category_detail',
|
'category_detail',
|
||||||
@ -430,6 +482,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
'suppliers',
|
'suppliers',
|
||||||
'thumbnail',
|
'thumbnail',
|
||||||
'trackable',
|
'trackable',
|
||||||
|
'unallocated_stock',
|
||||||
'units',
|
'units',
|
||||||
'variant_of',
|
'variant_of',
|
||||||
'virtual',
|
'virtual',
|
||||||
|
@ -37,13 +37,17 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if barcodes %}
|
{% if barcodes or labels_enabled %}
|
||||||
<!-- Barcode actions menu -->
|
<!-- Barcode actions menu -->
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
|
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
|
||||||
<ul class='dropdown-menu'>
|
<ul class='dropdown-menu'>
|
||||||
|
{% if barcodes %}
|
||||||
<li><a class='dropdown-item' href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if labels_enabled %}
|
||||||
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -424,9 +428,11 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{% if labels_enabled %}
|
||||||
$('#print-label').click(function() {
|
$('#print-label').click(function() {
|
||||||
printPartLabels([{{ part.pk }}]);
|
printPartLabels([{{ part.pk }}]);
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
function adjustPartStock(action) {
|
function adjustPartStock(action) {
|
||||||
inventreeGet(
|
inventreeGet(
|
||||||
|
@ -9,7 +9,7 @@ from rest_framework import status
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
from InvenTree.status_codes import StockStatus
|
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||||
|
|
||||||
from part.models import Part, PartCategory
|
from part.models import Part, PartCategory
|
||||||
from part.models import BomItem, BomItemSubstitute
|
from part.models import BomItem, BomItemSubstitute
|
||||||
@ -17,6 +17,9 @@ from stock.models import StockItem, StockLocation
|
|||||||
from company.models import Company
|
from company.models import Company
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
import build.models
|
||||||
|
import order.models
|
||||||
|
|
||||||
|
|
||||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
@ -247,7 +250,7 @@ class PartAPITest(InvenTreeAPITestCase):
|
|||||||
data = {'cascade': True}
|
data = {'cascade': True}
|
||||||
response = self.client.get(url, data, format='json')
|
response = self.client.get(url, data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 13)
|
self.assertEqual(len(response.data), Part.objects.count())
|
||||||
|
|
||||||
def test_get_parts_by_cat(self):
|
def test_get_parts_by_cat(self):
|
||||||
url = reverse('api-part-list')
|
url = reverse('api-part-list')
|
||||||
@ -815,6 +818,10 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
'location',
|
'location',
|
||||||
'bom',
|
'bom',
|
||||||
'test_templates',
|
'test_templates',
|
||||||
|
'build',
|
||||||
|
'location',
|
||||||
|
'stock',
|
||||||
|
'sales_order',
|
||||||
]
|
]
|
||||||
|
|
||||||
roles = [
|
roles = [
|
||||||
@ -826,6 +833,9 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
# Ensure the part "variant" tree is correctly structured
|
||||||
|
Part.objects.rebuild()
|
||||||
|
|
||||||
# Add a new part
|
# Add a new part
|
||||||
self.part = Part.objects.create(
|
self.part = Part.objects.create(
|
||||||
name='Banana',
|
name='Banana',
|
||||||
@ -880,6 +890,153 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(data['in_stock'], 1100)
|
self.assertEqual(data['in_stock'], 1100)
|
||||||
self.assertEqual(data['stock_item_count'], 105)
|
self.assertEqual(data['stock_item_count'], 105)
|
||||||
|
|
||||||
|
def test_allocation_annotations(self):
|
||||||
|
"""
|
||||||
|
Tests for query annotations which add allocation information.
|
||||||
|
Ref: https://github.com/inventree/InvenTree/pull/2797
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We are looking at Part ID 100 ("Bob")
|
||||||
|
url = reverse('api-part-detail', kwargs={'pk': 100})
|
||||||
|
|
||||||
|
part = Part.objects.get(pk=100)
|
||||||
|
|
||||||
|
response = self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
# Check that the expected annotated fields exist in the data
|
||||||
|
data = response.data
|
||||||
|
self.assertEqual(data['allocated_to_build_orders'], 0)
|
||||||
|
self.assertEqual(data['allocated_to_sales_orders'], 0)
|
||||||
|
|
||||||
|
# The unallocated stock count should equal the 'in stock' coutn
|
||||||
|
in_stock = data['in_stock']
|
||||||
|
self.assertEqual(in_stock, 126)
|
||||||
|
self.assertEqual(data['unallocated_stock'], in_stock)
|
||||||
|
|
||||||
|
# Check that model functions return the same values
|
||||||
|
self.assertEqual(part.build_order_allocation_count(), 0)
|
||||||
|
self.assertEqual(part.sales_order_allocation_count(), 0)
|
||||||
|
self.assertEqual(part.total_stock, in_stock)
|
||||||
|
self.assertEqual(part.available_stock, in_stock)
|
||||||
|
|
||||||
|
# Now, let's create a sales order, and allocate some stock
|
||||||
|
so = order.models.SalesOrder.objects.create(
|
||||||
|
reference='001',
|
||||||
|
customer=Company.objects.get(pk=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# We wish to send 50 units of "Bob" against this sales order
|
||||||
|
line = order.models.SalesOrderLineItem.objects.create(
|
||||||
|
quantity=50,
|
||||||
|
order=so,
|
||||||
|
part=part,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a shipment against the order
|
||||||
|
shipment_1 = order.models.SalesOrderShipment.objects.create(
|
||||||
|
order=so,
|
||||||
|
reference='001',
|
||||||
|
)
|
||||||
|
|
||||||
|
shipment_2 = order.models.SalesOrderShipment.objects.create(
|
||||||
|
order=so,
|
||||||
|
reference='002',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Allocate stock items to this order, against multiple shipments
|
||||||
|
order.models.SalesOrderAllocation.objects.create(
|
||||||
|
line=line,
|
||||||
|
shipment=shipment_1,
|
||||||
|
item=StockItem.objects.get(pk=1007),
|
||||||
|
quantity=17
|
||||||
|
)
|
||||||
|
|
||||||
|
order.models.SalesOrderAllocation.objects.create(
|
||||||
|
line=line,
|
||||||
|
shipment=shipment_1,
|
||||||
|
item=StockItem.objects.get(pk=1008),
|
||||||
|
quantity=18
|
||||||
|
)
|
||||||
|
|
||||||
|
order.models.SalesOrderAllocation.objects.create(
|
||||||
|
line=line,
|
||||||
|
shipment=shipment_2,
|
||||||
|
item=StockItem.objects.get(pk=1006),
|
||||||
|
quantity=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit the API request again - should show us the sales order allocation
|
||||||
|
data = self.get(url, expected_code=200).data
|
||||||
|
|
||||||
|
self.assertEqual(data['allocated_to_sales_orders'], 50)
|
||||||
|
self.assertEqual(data['in_stock'], 126)
|
||||||
|
self.assertEqual(data['unallocated_stock'], 76)
|
||||||
|
|
||||||
|
# Now, "ship" the first shipment (so the stock is not 'in stock' any more)
|
||||||
|
shipment_1.complete_shipment(None)
|
||||||
|
|
||||||
|
# Refresh the API data
|
||||||
|
data = self.get(url, expected_code=200).data
|
||||||
|
|
||||||
|
self.assertEqual(data['allocated_to_build_orders'], 0)
|
||||||
|
self.assertEqual(data['allocated_to_sales_orders'], 15)
|
||||||
|
self.assertEqual(data['in_stock'], 91)
|
||||||
|
self.assertEqual(data['unallocated_stock'], 76)
|
||||||
|
|
||||||
|
# Next, we create a build order and allocate stock against it
|
||||||
|
bo = build.models.Build.objects.create(
|
||||||
|
part=Part.objects.get(pk=101),
|
||||||
|
quantity=10,
|
||||||
|
title='Making some assemblies',
|
||||||
|
status=BuildStatus.PRODUCTION,
|
||||||
|
)
|
||||||
|
|
||||||
|
bom_item = BomItem.objects.get(pk=6)
|
||||||
|
|
||||||
|
# Allocate multiple stock items against this build order
|
||||||
|
build.models.BuildItem.objects.create(
|
||||||
|
build=bo,
|
||||||
|
bom_item=bom_item,
|
||||||
|
stock_item=StockItem.objects.get(pk=1000),
|
||||||
|
quantity=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Request data once more
|
||||||
|
data = self.get(url, expected_code=200).data
|
||||||
|
|
||||||
|
self.assertEqual(data['allocated_to_build_orders'], 10)
|
||||||
|
self.assertEqual(data['allocated_to_sales_orders'], 15)
|
||||||
|
self.assertEqual(data['in_stock'], 91)
|
||||||
|
self.assertEqual(data['unallocated_stock'], 66)
|
||||||
|
|
||||||
|
# Again, check that the direct model functions return the same values
|
||||||
|
self.assertEqual(part.build_order_allocation_count(), 10)
|
||||||
|
self.assertEqual(part.sales_order_allocation_count(), 15)
|
||||||
|
self.assertEqual(part.total_stock, 91)
|
||||||
|
self.assertEqual(part.available_stock, 66)
|
||||||
|
|
||||||
|
# Allocate further stock against the build
|
||||||
|
build.models.BuildItem.objects.create(
|
||||||
|
build=bo,
|
||||||
|
bom_item=bom_item,
|
||||||
|
stock_item=StockItem.objects.get(pk=1001),
|
||||||
|
quantity=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Request data once more
|
||||||
|
data = self.get(url, expected_code=200).data
|
||||||
|
|
||||||
|
self.assertEqual(data['allocated_to_build_orders'], 20)
|
||||||
|
self.assertEqual(data['allocated_to_sales_orders'], 15)
|
||||||
|
self.assertEqual(data['in_stock'], 91)
|
||||||
|
self.assertEqual(data['unallocated_stock'], 56)
|
||||||
|
|
||||||
|
# Again, check that the direct model functions return the same values
|
||||||
|
self.assertEqual(part.build_order_allocation_count(), 20)
|
||||||
|
self.assertEqual(part.sales_order_allocation_count(), 15)
|
||||||
|
self.assertEqual(part.total_stock, 91)
|
||||||
|
self.assertEqual(part.available_stock, 56)
|
||||||
|
|
||||||
|
|
||||||
class BomItemTest(InvenTreeAPITestCase):
|
class BomItemTest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -46,7 +46,7 @@ class BomItemTest(TestCase):
|
|||||||
# TODO: Tests for multi-level BOMs
|
# TODO: Tests for multi-level BOMs
|
||||||
|
|
||||||
def test_used_in(self):
|
def test_used_in(self):
|
||||||
self.assertEqual(self.bob.used_in_count, 0)
|
self.assertEqual(self.bob.used_in_count, 1)
|
||||||
self.assertEqual(self.orphan.used_in_count, 1)
|
self.assertEqual(self.orphan.used_in_count, 1)
|
||||||
|
|
||||||
def test_self_reference(self):
|
def test_self_reference(self):
|
||||||
|
@ -251,3 +251,104 @@
|
|||||||
rght: 0
|
rght: 0
|
||||||
expiry_date: "1990-10-10"
|
expiry_date: "1990-10-10"
|
||||||
status: 70
|
status: 70
|
||||||
|
|
||||||
|
# Multiple stock items for "Bob" (PK 100)
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1000
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 1
|
||||||
|
quantity: 10
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1001
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 1
|
||||||
|
quantity: 11
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1002
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 1
|
||||||
|
quantity: 12
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1003
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 1
|
||||||
|
quantity: 13
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1004
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 1
|
||||||
|
quantity: 14
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1005
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 1
|
||||||
|
quantity: 15
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1006
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 1
|
||||||
|
quantity: 16
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1007
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 7
|
||||||
|
quantity: 17
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
||||||
|
- model: stock.stockitem
|
||||||
|
pk: 1008
|
||||||
|
fields:
|
||||||
|
part: 100
|
||||||
|
location: 7
|
||||||
|
quantity: 18
|
||||||
|
level: 0
|
||||||
|
tree_id: 0
|
||||||
|
lft: 0
|
||||||
|
rght: 0
|
||||||
|
|
@ -49,15 +49,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Document / label menu -->
|
<!-- Document / label menu -->
|
||||||
|
{% if test_report_enabled or labels_enabled %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button id='document-options' title='{% trans "Printing actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-print'></span> <span class='caret'></span></button>
|
<button id='document-options' title='{% trans "Printing actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-print'></span> <span class='caret'></span></button>
|
||||||
<ul class='dropdown-menu' role='menu'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
|
{% if labels_enabled %}
|
||||||
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% if test_report_enabled %}
|
{% if test_report_enabled %}
|
||||||
<li><a class='dropdown-item' href='#' id='stock-test-report'><span class='fas fa-file-pdf'></span> {% trans "Test Report" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='stock-test-report'><span class='fas fa-file-pdf'></span> {% trans "Test Report" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Stock adjustment menu -->
|
<!-- Stock adjustment menu -->
|
||||||
{% if user_owns_item %}
|
{% if user_owns_item %}
|
||||||
{% if roles.stock.change and not item.is_building %}
|
{% if roles.stock.change and not item.is_building %}
|
||||||
|
@ -34,7 +34,9 @@
|
|||||||
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
|
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
|
||||||
<ul class='dropdown-menu'>
|
<ul class='dropdown-menu'>
|
||||||
<li><a class='dropdown-item' href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
|
||||||
|
{% if labels_enabled %}
|
||||||
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
<li><a class='dropdown-item' href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -181,6 +183,7 @@
|
|||||||
<div id='sublocation-button-toolbar'>
|
<div id='sublocation-button-toolbar'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<!-- Printing actions menu -->
|
<!-- Printing actions menu -->
|
||||||
|
{% if labels_enabled %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button id='location-print-options' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle="dropdown" title='{% trans "Printing Actions" %}'>
|
<button id='location-print-options' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle="dropdown" title='{% trans "Printing Actions" %}'>
|
||||||
<span class='fas fa-print'></span> <span class='caret'></span>
|
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||||
@ -189,6 +192,7 @@
|
|||||||
<li><a class='dropdown-item' href='#' id='multi-location-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-location-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% include "filter_list.html" with id="location" %}
|
{% include "filter_list.html" with id="location" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -222,6 +226,15 @@
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
{% if labels_enabled %}
|
||||||
|
$('#print-label').click(function() {
|
||||||
|
|
||||||
|
var locs = [{{ location.pk }}];
|
||||||
|
|
||||||
|
printStockLocationLabels(locs);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
$('#multi-location-print-label').click(function() {
|
$('#multi-location-print-label').click(function() {
|
||||||
|
|
||||||
var selections = $('#sublocation-table').bootstrapTable('getSelections');
|
var selections = $('#sublocation-table').bootstrapTable('getSelections');
|
||||||
@ -234,6 +247,7 @@
|
|||||||
|
|
||||||
printStockLocationLabels(locations);
|
printStockLocationLabels(locations);
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if location %}
|
{% if location %}
|
||||||
$("#barcode-check-in").click(function() {
|
$("#barcode-check-in").click(function() {
|
||||||
@ -298,14 +312,6 @@
|
|||||||
adjustLocationStock('move');
|
adjustLocationStock('move');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#print-label').click(function() {
|
|
||||||
|
|
||||||
var locs = [{{ location.pk }}];
|
|
||||||
|
|
||||||
printStockLocationLabels(locs);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$('#show-qr-code').click(function() {
|
$('#show-qr-code').click(function() {
|
||||||
|
@ -104,7 +104,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
response = self.get_stock()
|
response = self.get_stock()
|
||||||
|
|
||||||
self.assertEqual(len(response), 20)
|
self.assertEqual(len(response), 29)
|
||||||
|
|
||||||
def test_filter_by_part(self):
|
def test_filter_by_part(self):
|
||||||
"""
|
"""
|
||||||
@ -113,7 +113,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
response = self.get_stock(part=25)
|
response = self.get_stock(part=25)
|
||||||
|
|
||||||
self.assertEqual(len(response), 8)
|
self.assertEqual(len(response), 17)
|
||||||
|
|
||||||
response = self.get_stock(part=10004)
|
response = self.get_stock(part=10004)
|
||||||
|
|
||||||
@ -136,13 +136,13 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertEqual(len(response), 1)
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
response = self.get_stock(location=1, cascade=0)
|
response = self.get_stock(location=1, cascade=0)
|
||||||
self.assertEqual(len(response), 0)
|
self.assertEqual(len(response), 7)
|
||||||
|
|
||||||
response = self.get_stock(location=1, cascade=1)
|
response = self.get_stock(location=1, cascade=1)
|
||||||
self.assertEqual(len(response), 2)
|
self.assertEqual(len(response), 9)
|
||||||
|
|
||||||
response = self.get_stock(location=7)
|
response = self.get_stock(location=7)
|
||||||
self.assertEqual(len(response), 16)
|
self.assertEqual(len(response), 18)
|
||||||
|
|
||||||
def test_filter_by_depleted(self):
|
def test_filter_by_depleted(self):
|
||||||
"""
|
"""
|
||||||
@ -153,7 +153,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertEqual(len(response), 1)
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
response = self.get_stock(depleted=0)
|
response = self.get_stock(depleted=0)
|
||||||
self.assertEqual(len(response), 19)
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
def test_filter_by_in_stock(self):
|
def test_filter_by_in_stock(self):
|
||||||
"""
|
"""
|
||||||
@ -161,7 +161,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.get_stock(in_stock=1)
|
response = self.get_stock(in_stock=1)
|
||||||
self.assertEqual(len(response), 17)
|
self.assertEqual(len(response), 26)
|
||||||
|
|
||||||
response = self.get_stock(in_stock=0)
|
response = self.get_stock(in_stock=0)
|
||||||
self.assertEqual(len(response), 3)
|
self.assertEqual(len(response), 3)
|
||||||
@ -172,7 +172,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
codes = {
|
codes = {
|
||||||
StockStatus.OK: 18,
|
StockStatus.OK: 27,
|
||||||
StockStatus.DESTROYED: 1,
|
StockStatus.DESTROYED: 1,
|
||||||
StockStatus.LOST: 1,
|
StockStatus.LOST: 1,
|
||||||
StockStatus.DAMAGED: 0,
|
StockStatus.DAMAGED: 0,
|
||||||
@ -205,7 +205,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertIsNotNone(item['serial'])
|
self.assertIsNotNone(item['serial'])
|
||||||
|
|
||||||
response = self.get_stock(serialized=0)
|
response = self.get_stock(serialized=0)
|
||||||
self.assertEqual(len(response), 8)
|
self.assertEqual(len(response), 17)
|
||||||
|
|
||||||
for item in response:
|
for item in response:
|
||||||
self.assertIsNone(item['serial'])
|
self.assertIsNone(item['serial'])
|
||||||
@ -217,7 +217,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
# First, we can assume that the 'stock expiry' feature is disabled
|
# First, we can assume that the 'stock expiry' feature is disabled
|
||||||
response = self.get_stock(expired=1)
|
response = self.get_stock(expired=1)
|
||||||
self.assertEqual(len(response), 20)
|
self.assertEqual(len(response), 29)
|
||||||
|
|
||||||
self.user.is_staff = True
|
self.user.is_staff = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
@ -232,7 +232,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertTrue(item['expired'])
|
self.assertTrue(item['expired'])
|
||||||
|
|
||||||
response = self.get_stock(expired=0)
|
response = self.get_stock(expired=0)
|
||||||
self.assertEqual(len(response), 19)
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
for item in response:
|
for item in response:
|
||||||
self.assertFalse(item['expired'])
|
self.assertFalse(item['expired'])
|
||||||
@ -249,7 +249,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertEqual(len(response), 4)
|
self.assertEqual(len(response), 4)
|
||||||
|
|
||||||
response = self.get_stock(expired=0)
|
response = self.get_stock(expired=0)
|
||||||
self.assertEqual(len(response), 16)
|
self.assertEqual(len(response), 25)
|
||||||
|
|
||||||
def test_paginate(self):
|
def test_paginate(self):
|
||||||
"""
|
"""
|
||||||
@ -290,7 +290,8 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
dataset = self.export_data({})
|
dataset = self.export_data({})
|
||||||
|
|
||||||
self.assertEqual(len(dataset), 20)
|
# Check that *all* stock item objects have been exported
|
||||||
|
self.assertEqual(len(dataset), StockItem.objects.count())
|
||||||
|
|
||||||
# Expected headers
|
# Expected headers
|
||||||
headers = [
|
headers = [
|
||||||
@ -308,11 +309,11 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
# Now, add a filter to the results
|
# Now, add a filter to the results
|
||||||
dataset = self.export_data({'location': 1})
|
dataset = self.export_data({'location': 1})
|
||||||
|
|
||||||
self.assertEqual(len(dataset), 2)
|
self.assertEqual(len(dataset), 9)
|
||||||
|
|
||||||
dataset = self.export_data({'part': 25})
|
dataset = self.export_data({'part': 25})
|
||||||
|
|
||||||
self.assertEqual(len(dataset), 8)
|
self.assertEqual(len(dataset), 17)
|
||||||
|
|
||||||
|
|
||||||
class StockItemTest(StockAPITestCase):
|
class StockItemTest(StockAPITestCase):
|
||||||
|
@ -167,8 +167,8 @@ class StockTest(TestCase):
|
|||||||
self.assertFalse(self.drawer2.has_items())
|
self.assertFalse(self.drawer2.has_items())
|
||||||
|
|
||||||
# Drawer 3 should have three stock items
|
# Drawer 3 should have three stock items
|
||||||
self.assertEqual(self.drawer3.stock_items.count(), 16)
|
self.assertEqual(self.drawer3.stock_items.count(), 18)
|
||||||
self.assertEqual(self.drawer3.item_count, 16)
|
self.assertEqual(self.drawer3.item_count, 18)
|
||||||
|
|
||||||
def test_stock_count(self):
|
def test_stock_count(self):
|
||||||
part = Part.objects.get(pk=1)
|
part = Part.objects.get(pk=1)
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<div class='row'>
|
<div class='row'>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="LABEL_ENABLE" icon='fa-toggle-on' user_setting=True %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LABEL_INLINE" icon='fa-tag' user_setting=True %}
|
{% include "InvenTree/settings/setting.html" with key="LABEL_INLINE" icon='fa-tag' user_setting=True %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -14,8 +14,16 @@
|
|||||||
<div class='row'>
|
<div class='row'>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_PARTS" user_setting=True icon='fa-shapes' %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_CATEGORIES" user_setting=True icon='fa-sitemap' %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_STOCK" user_setting=True icon='fa-boxes' %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_LOCATIONS" user_setting=True icon='fa-sitemap' %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_COMPANIES" user_setting=True icon='fa-building' %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS" user_setting=True icon='fa-shopping-cart' %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_SALES_ORDERS" user_setting=True icon='fa-truck' %}
|
||||||
|
|
||||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" user_setting=True icon='fa-search' %}
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" user_setting=True icon='fa-search' %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_SHOW_STOCK_LEVELS" user_setting=True icon='fa-boxes' %}
|
|
||||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_HIDE_INACTIVE_PARTS" user_setting=True icon='fa-eye-slash' %}
|
{% include "InvenTree/settings/setting.html" with key="SEARCH_HIDE_INACTIVE_PARTS" user_setting=True icon='fa-eye-slash' %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
|
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
|
||||||
{% settings_value "REPORT_ENABLE" as report_enabled %}
|
{% settings_value "REPORT_ENABLE" as report_enabled %}
|
||||||
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
|
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
|
||||||
|
{% settings_value "LABEL_ENABLE" with user=user as labels_enabled %}
|
||||||
{% inventree_demo_mode as demo_mode %}
|
{% inventree_demo_mode as demo_mode %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -126,9 +127,11 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% include 'modals.html' %}
|
||||||
{% include 'about.html' %}
|
{% include 'about.html' %}
|
||||||
{% include "notifications.html" %}
|
{% include "notifications.html" %}
|
||||||
|
{% include "search.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
@ -185,6 +188,7 @@
|
|||||||
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% i18n_static 'part.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'part.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% i18n_static 'report.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'report.js' %}"></script>
|
||||||
|
<script type='text/javascript' src="{% i18n_static 'search.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% i18n_static 'stock.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'stock.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% i18n_static 'plugin.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'plugin.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
|
<script type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
editSetting,
|
editSetting,
|
||||||
user_settings,
|
user_settings,
|
||||||
global_settings,
|
global_settings,
|
||||||
|
plugins_enabled,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
{% user_settings request.user as USER_SETTINGS %}
|
{% user_settings request.user as USER_SETTINGS %}
|
||||||
@ -20,6 +21,13 @@ const global_settings = {
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
{% plugins_enabled as p_en %}
|
||||||
|
{% if p_en %}
|
||||||
|
const plugins_enabled = true;
|
||||||
|
{% else %}
|
||||||
|
const plugins_enabled = false;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Edit a setting value
|
* Edit a setting value
|
||||||
*/
|
*/
|
||||||
|
@ -1746,7 +1746,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
required: true,
|
required: true,
|
||||||
render_part_detail: true,
|
render_part_detail: true,
|
||||||
render_location_detail: true,
|
render_location_detail: true,
|
||||||
render_stock_id: false,
|
render_pk: false,
|
||||||
auto_fill: true,
|
auto_fill: true,
|
||||||
auto_fill_filters: auto_fill_filters,
|
auto_fill_filters: auto_fill_filters,
|
||||||
onSelect: function(data, field, opts) {
|
onSelect: function(data, field, opts) {
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
modalSetTitle,
|
modalSetTitle,
|
||||||
modalSubmit,
|
modalSubmit,
|
||||||
openModal,
|
openModal,
|
||||||
|
plugins_enabled,
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -232,26 +233,28 @@ function selectLabel(labels, items, options={}) {
|
|||||||
var plugins = [];
|
var plugins = [];
|
||||||
|
|
||||||
// Request a list of available label printing plugins from the server
|
// Request a list of available label printing plugins from the server
|
||||||
inventreeGet(
|
if (plugins_enabled) {
|
||||||
`/api/plugin/`,
|
inventreeGet(
|
||||||
{},
|
`/api/plugin/`,
|
||||||
{
|
{},
|
||||||
async: false,
|
{
|
||||||
success: function(response) {
|
async: false,
|
||||||
response.forEach(function(plugin) {
|
success: function(response) {
|
||||||
// Look for active plugins which implement the 'labels' mixin class
|
response.forEach(function(plugin) {
|
||||||
if (plugin.active && plugin.mixins && plugin.mixins.labels) {
|
// Look for active plugins which implement the 'labels' mixin class
|
||||||
// This plugin supports label printing
|
if (plugin.active && plugin.mixins && plugin.mixins.labels) {
|
||||||
plugins.push(plugin);
|
// This plugin supports label printing
|
||||||
}
|
plugins.push(plugin);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
var plugin_selection = '';
|
var plugin_selection = '';
|
||||||
|
|
||||||
if (plugins.length > 0) {
|
if (plugins_enabled && plugins.length > 0) {
|
||||||
plugin_selection =`
|
plugin_selection =`
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label class='control-label requiredField' for='id_plugin'>
|
<label class='control-label requiredField' for='id_plugin'>
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
renderCompany,
|
renderCompany,
|
||||||
renderManufacturerPart,
|
renderManufacturerPart,
|
||||||
renderOwner,
|
renderOwner,
|
||||||
|
renderPart,
|
||||||
renderPartCategory,
|
renderPartCategory,
|
||||||
|
renderStockItem,
|
||||||
renderStockLocation,
|
renderStockLocation,
|
||||||
renderSupplierPart,
|
renderSupplierPart,
|
||||||
*/
|
*/
|
||||||
@ -29,15 +31,33 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// Should the ID be rendered for this string
|
||||||
|
function renderId(title, pk, parameters={}) {
|
||||||
|
|
||||||
|
// Default = true
|
||||||
|
var render = true;
|
||||||
|
|
||||||
|
if ('render_pk' in parameters) {
|
||||||
|
render = parameters['render_pk'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (render) {
|
||||||
|
return `<span class='float-right'><small>${title}: ${pk}</small></span>`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Renderer for "Company" model
|
// Renderer for "Company" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderCompany(name, data, parameters, options) {
|
function renderCompany(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var html = select2Thumbnail(data.image);
|
var html = select2Thumbnail(data.image);
|
||||||
|
|
||||||
html += `<span><b>${data.name}</b></span> - <i>${data.description}</i>`;
|
html += `<span><b>${data.name}</b></span> - <i>${data.description}</i>`;
|
||||||
|
|
||||||
html += `<span class='float-right'><small>{% trans "Company ID" %}: ${data.pk}</small></span>`;
|
html += renderId('{% trans "Company ID" %}', data.pk, parameters);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -45,7 +65,7 @@ function renderCompany(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "StockItem" model
|
// Renderer for "StockItem" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderStockItem(name, data, parameters, options) {
|
function renderStockItem(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var image = blankImage();
|
var image = blankImage();
|
||||||
|
|
||||||
@ -65,18 +85,6 @@ function renderStockItem(name, data, parameters, options) {
|
|||||||
part_detail = `<img src='${image}' class='select2-thumbnail'><span>${data.part_detail.full_name}</span> - `;
|
part_detail = `<img src='${image}' class='select2-thumbnail'><span>${data.part_detail.full_name}</span> - `;
|
||||||
}
|
}
|
||||||
|
|
||||||
var render_stock_id = true;
|
|
||||||
|
|
||||||
if ('render_stock_id' in parameters) {
|
|
||||||
render_stock_id = parameters['render_stock_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
var stock_id = '';
|
|
||||||
|
|
||||||
if (render_stock_id) {
|
|
||||||
stock_id = `<span class='float-right'><small>{% trans "Stock ID" %}: ${data.pk}</small></span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
var render_location_detail = false;
|
var render_location_detail = false;
|
||||||
|
|
||||||
if ('render_location_detail' in parameters) {
|
if ('render_location_detail' in parameters) {
|
||||||
@ -86,7 +94,7 @@ function renderStockItem(name, data, parameters, options) {
|
|||||||
var location_detail = '';
|
var location_detail = '';
|
||||||
|
|
||||||
if (render_location_detail && data.location_detail) {
|
if (render_location_detail && data.location_detail) {
|
||||||
location_detail = ` - (<em>${data.location_detail.name}</em>)`;
|
location_detail = ` <small>- (<em>${data.location_detail.name}</em>)</small>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
var stock_detail = '';
|
var stock_detail = '';
|
||||||
@ -101,7 +109,10 @@ function renderStockItem(name, data, parameters, options) {
|
|||||||
|
|
||||||
var html = `
|
var html = `
|
||||||
<span>
|
<span>
|
||||||
${part_detail}${stock_detail}${location_detail}${stock_id}
|
${part_detail}
|
||||||
|
${stock_detail}
|
||||||
|
${location_detail}
|
||||||
|
${renderId('{% trans "Stock ID" %}', data.pk, parameters)}
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -111,7 +122,7 @@ function renderStockItem(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "StockLocation" model
|
// Renderer for "StockLocation" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderStockLocation(name, data, parameters, options) {
|
function renderStockLocation(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var level = '- '.repeat(data.level);
|
var level = '- '.repeat(data.level);
|
||||||
|
|
||||||
@ -133,7 +144,7 @@ function renderStockLocation(name, data, parameters, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderBuild(name, data, parameters, options) {
|
function renderBuild(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var image = null;
|
var image = null;
|
||||||
|
|
||||||
@ -154,7 +165,7 @@ function renderBuild(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "Part" model
|
// Renderer for "Part" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderPart(name, data, parameters, options) {
|
function renderPart(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var html = select2Thumbnail(data.image);
|
var html = select2Thumbnail(data.image);
|
||||||
|
|
||||||
@ -164,13 +175,14 @@ function renderPart(name, data, parameters, options) {
|
|||||||
html += ` - <i><small>${data.description}</small></i>`;
|
html += ` - <i><small>${data.description}</small></i>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extra = '';
|
var stock_data = '';
|
||||||
|
|
||||||
// Display available part quantity
|
|
||||||
if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) {
|
if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) {
|
||||||
extra += partStockLabel(data);
|
stock_data = partStockLabel(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var extra = '';
|
||||||
|
|
||||||
if (!data.active) {
|
if (!data.active) {
|
||||||
extra += `<span class='badge badge-right rounded-pill bg-danger'>{% trans "Inactive" %}</span>`;
|
extra += `<span class='badge badge-right rounded-pill bg-danger'>{% trans "Inactive" %}</span>`;
|
||||||
}
|
}
|
||||||
@ -178,8 +190,9 @@ function renderPart(name, data, parameters, options) {
|
|||||||
html += `
|
html += `
|
||||||
<span class='float-right'>
|
<span class='float-right'>
|
||||||
<small>
|
<small>
|
||||||
|
${stock_data}
|
||||||
${extra}
|
${extra}
|
||||||
{% trans "Part ID" %}: ${data.pk}
|
${renderId('{% trans "Part ID" $}', data.pk, parameters)}
|
||||||
</small>
|
</small>
|
||||||
</span>`;
|
</span>`;
|
||||||
|
|
||||||
@ -188,7 +201,7 @@ function renderPart(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "User" model
|
// Renderer for "User" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderUser(name, data, parameters, options) {
|
function renderUser(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var html = `<span>${data.username}</span>`;
|
var html = `<span>${data.username}</span>`;
|
||||||
|
|
||||||
@ -202,7 +215,7 @@ function renderUser(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "Owner" model
|
// Renderer for "Owner" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderOwner(name, data, parameters, options) {
|
function renderOwner(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var html = `<span>${data.name}</span>`;
|
var html = `<span>${data.name}</span>`;
|
||||||
|
|
||||||
@ -223,15 +236,13 @@ function renderOwner(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "PurchaseOrder" model
|
// Renderer for "PurchaseOrder" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderPurchaseOrder(name, data, parameters, options) {
|
function renderPurchaseOrder(name, data, parameters={}, options={}) {
|
||||||
var html = '';
|
|
||||||
|
|
||||||
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
|
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
|
||||||
|
var html = `<span>${prefix}${data.reference}</span>`;
|
||||||
|
|
||||||
var thumbnail = null;
|
var thumbnail = null;
|
||||||
|
|
||||||
html += `<span>${prefix}${data.reference}</span>`;
|
|
||||||
|
|
||||||
if (data.supplier_detail) {
|
if (data.supplier_detail) {
|
||||||
thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image;
|
thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image;
|
||||||
|
|
||||||
@ -243,13 +254,7 @@ function renderPurchaseOrder(name, data, parameters, options) {
|
|||||||
html += ` - <em>${data.description}</em>`;
|
html += ` - <em>${data.description}</em>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `
|
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
|
||||||
<span class='float-right'>
|
|
||||||
<small>
|
|
||||||
{% trans "Order ID" %}: ${data.pk}
|
|
||||||
</small>
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -257,19 +262,25 @@ function renderPurchaseOrder(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "SalesOrder" model
|
// Renderer for "SalesOrder" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderSalesOrder(name, data, parameters, options) {
|
function renderSalesOrder(name, data, parameters={}, options={}) {
|
||||||
var html = `<span>${data.reference}</span>`;
|
|
||||||
|
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
|
||||||
|
var html = `<span>${prefix}${data.reference}</span>`;
|
||||||
|
|
||||||
|
var thumbnail = null;
|
||||||
|
|
||||||
|
if (data.customer_detail) {
|
||||||
|
thumbnail = data.customer_detail.thumbnail || data.customer_detail.image;
|
||||||
|
|
||||||
|
html += ' - ' + select2Thumbnail(thumbnail);
|
||||||
|
html += `<span>${data.customer_detail.name}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.description) {
|
if (data.description) {
|
||||||
html += ` - <em>${data.description}</em>`;
|
html += ` - <em>${data.description}</em>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `
|
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
|
||||||
<span class='float-right'>
|
|
||||||
<small>
|
|
||||||
{% trans "Order ID" %}: ${data.pk}
|
|
||||||
</small>
|
|
||||||
</span>`;
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -277,7 +288,7 @@ function renderSalesOrder(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "SalesOrderShipment" model
|
// Renderer for "SalesOrderShipment" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderSalesOrderShipment(name, data, parameters, options) {
|
function renderSalesOrderShipment(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
|
var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
|
||||||
|
|
||||||
@ -294,7 +305,7 @@ function renderSalesOrderShipment(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "PartCategory" model
|
// Renderer for "PartCategory" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderPartCategory(name, data, parameters, options) {
|
function renderPartCategory(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var level = '- '.repeat(data.level);
|
var level = '- '.repeat(data.level);
|
||||||
|
|
||||||
@ -310,7 +321,7 @@ function renderPartCategory(name, data, parameters, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderPartParameterTemplate(name, data, parameters, options) {
|
function renderPartParameterTemplate(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var units = '';
|
var units = '';
|
||||||
|
|
||||||
@ -326,7 +337,7 @@ function renderPartParameterTemplate(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "ManufacturerPart" model
|
// Renderer for "ManufacturerPart" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderManufacturerPart(name, data, parameters, options) {
|
function renderManufacturerPart(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var manufacturer_image = null;
|
var manufacturer_image = null;
|
||||||
var part_image = null;
|
var part_image = null;
|
||||||
@ -355,7 +366,7 @@ function renderManufacturerPart(name, data, parameters, options) {
|
|||||||
|
|
||||||
// Renderer for "SupplierPart" model
|
// Renderer for "SupplierPart" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderSupplierPart(name, data, parameters, options) {
|
function renderSupplierPart(name, data, parameters={}, options={}) {
|
||||||
|
|
||||||
var supplier_image = null;
|
var supplier_image = null;
|
||||||
var part_image = null;
|
var part_image = null;
|
||||||
|
@ -491,13 +491,50 @@ function duplicateBom(part_id, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a "badge" label showing stock information for this particular part
|
||||||
|
*/
|
||||||
function partStockLabel(part, options={}) {
|
function partStockLabel(part, options={}) {
|
||||||
|
|
||||||
if (part.in_stock) {
|
if (part.in_stock) {
|
||||||
return `<span class='badge rounded-pill bg-success ${options.classes}'>{% trans "Stock" %}: ${part.in_stock}</span>`;
|
// There IS stock available for this part
|
||||||
|
|
||||||
|
// Is stock "low" (below the 'minimum_stock' quantity)?
|
||||||
|
if ((part.minimum_stock > 0) && (part.minimum_stock > part.in_stock)) {
|
||||||
|
return `<span class='badge rounded-pill bg-warning ${options.classes}'>{% trans "Low stock" %}: ${part.in_stock}${part.units}</span>`;
|
||||||
|
} else if (part.unallocated_stock == 0) {
|
||||||
|
if (part.ordering) {
|
||||||
|
// There is no available stock, but stock is on order
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "On Order" %}: ${part.ordering}${part.units}</span>`;
|
||||||
|
} else if (part.building) {
|
||||||
|
// There is no available stock, but stock is being built
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "Building" %}: ${part.building}${part.units}</span>`;
|
||||||
|
} else {
|
||||||
|
// There is no available stock at all
|
||||||
|
return `<span class='badge rounded-pill bg-warning ${options.classes}'>{% trans "No stock available" %}</span>`;
|
||||||
|
}
|
||||||
|
} else if (part.unallocated_stock < part.in_stock) {
|
||||||
|
// Unallocated quanttiy is less than total quantity
|
||||||
|
return `<span class='badge rounded-pill bg-success ${options.classes}'>{% trans "Available" %}: ${part.unallocated_stock}/${part.in_stock}${part.units}</span>`;
|
||||||
|
} else {
|
||||||
|
// Stock is completely available
|
||||||
|
return `<span class='badge rounded-pill bg-success ${options.classes}'>{% trans "Available" %}: ${part.unallocated_stock}${part.units}</span>`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return `<span class='badge rounded-pill bg-danger ${options.classes}'>{% trans "No Stock" %}</span>`;
|
// There IS NO stock available for this part
|
||||||
|
|
||||||
|
if (part.ordering) {
|
||||||
|
// There is no stock, but stock is on order
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "On Order" %}: ${part.ordering}${part.units}</span>`;
|
||||||
|
} else if (part.building) {
|
||||||
|
// There is no stock, but stock is being built
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "Building" %}: ${part.building}${part.units}</span>`;
|
||||||
|
} else {
|
||||||
|
// There is no stock
|
||||||
|
return `<span class='badge rounded-pill bg-danger ${options.classes}'>{% trans "No Stock" %}</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1160,12 +1197,14 @@ function partGridTile(part) {
|
|||||||
|
|
||||||
if (!part.in_stock) {
|
if (!part.in_stock) {
|
||||||
stock = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
stock = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
||||||
|
} else if (!part.unallocated_stock) {
|
||||||
|
stock = `<span class='badge rounded-pill bg-warning'>{% trans "Not available" %}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
rows += `<tr><td><b>{% trans "Stock" %}</b></td><td>${stock}</td></tr>`;
|
rows += `<tr><td><b>{% trans "Stock" %}</b></td><td>${stock}</td></tr>`;
|
||||||
|
|
||||||
if (part.on_order) {
|
if (part.ordering) {
|
||||||
rows += `<tr><td><b>{$ trans "On Order" %}</b></td><td>${part.on_order}</td></tr>`;
|
rows += `<tr><td><b>{% trans "On Order" %}</b></td><td>${part.ordering}</td></tr>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.building) {
|
if (part.building) {
|
||||||
@ -1322,31 +1361,47 @@ function loadPartTable(table, url, options={}) {
|
|||||||
columns.push(col);
|
columns.push(col);
|
||||||
|
|
||||||
col = {
|
col = {
|
||||||
field: 'in_stock',
|
field: 'unallocated_stock',
|
||||||
title: '{% trans "Stock" %}',
|
title: '{% trans "Stock" %}',
|
||||||
searchable: false,
|
searchable: false,
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
var link = '?display=part-stock';
|
var link = '?display=part-stock';
|
||||||
|
|
||||||
if (value) {
|
if (row.in_stock) {
|
||||||
// There IS stock available for this part
|
// There IS stock available for this part
|
||||||
|
|
||||||
// Is stock "low" (below the 'minimum_stock' quantity)?
|
// Is stock "low" (below the 'minimum_stock' quantity)?
|
||||||
if (row.minimum_stock && row.minimum_stock > value) {
|
if (row.minimum_stock && row.minimum_stock > row.in_stock) {
|
||||||
value += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Low stock" %}</span>`;
|
value += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Low stock" %}</span>`;
|
||||||
|
} else if (value == 0) {
|
||||||
|
if (row.ordering) {
|
||||||
|
// There is no available stock, but stock is on order
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "On Order" %}: ${row.ordering}</span>`;
|
||||||
|
link = '?display=purchase-orders';
|
||||||
|
} else if (row.building) {
|
||||||
|
// There is no available stock, but stock is being built
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "Building" %}: ${row.building}</span>`;
|
||||||
|
link = '?display=build-orders';
|
||||||
|
} else {
|
||||||
|
// There is no available stock
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-warning'>{% trans "No stock available" %}</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (row.on_order) {
|
|
||||||
// There is no stock available, but stock is on order
|
|
||||||
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "On Order" %}: ${row.on_order}</span>`;
|
|
||||||
link = '?display=purchase-orders';
|
|
||||||
} else if (row.building) {
|
|
||||||
// There is no stock available, but stock is being built
|
|
||||||
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "Building" %}: ${row.building}</span>`;
|
|
||||||
link = '?display=build-orders';
|
|
||||||
} else {
|
} else {
|
||||||
// There is no stock available
|
// There IS NO stock available for this part
|
||||||
value = `0<span class='badge badge-right rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
|
||||||
|
if (row.ordering) {
|
||||||
|
// There is no stock, but stock is on order
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "On Order" %}: ${row.ordering}</span>`;
|
||||||
|
link = '?display=purchase-orders';
|
||||||
|
} else if (row.building) {
|
||||||
|
// There is no stock, but stock is being built
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "Building" %}: ${row.building}</span>`;
|
||||||
|
link = '?display=build-orders';
|
||||||
|
} else {
|
||||||
|
// There is no stock
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderLink(value, `/part/${row.pk}/${link}`);
|
return renderLink(value, `/part/${row.pk}/${link}`);
|
||||||
|
329
InvenTree/templates/js/translated/search.js
Normal file
329
InvenTree/templates/js/translated/search.js
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
/* globals
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* exported
|
||||||
|
closeSearchPanel,
|
||||||
|
openSearchPanel,
|
||||||
|
searchTextChanged,
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback when the search panel is closed
|
||||||
|
*/
|
||||||
|
function closeSearchPanel() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback when the search panel is opened.
|
||||||
|
* Ensure the panel is in a known state
|
||||||
|
*/
|
||||||
|
function openSearchPanel() {
|
||||||
|
|
||||||
|
var panel = $('#offcanvas-search');
|
||||||
|
|
||||||
|
clearSearchResults();
|
||||||
|
|
||||||
|
panel.find('#search-input').on('keyup change', searchTextChanged);
|
||||||
|
|
||||||
|
// Callback for "clear search" button
|
||||||
|
panel.find('#search-clear').click(function(event) {
|
||||||
|
|
||||||
|
// Prevent this button from actually submitting the form
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
panel.find('#search-input').val('');
|
||||||
|
clearSearchResults();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for the "close search" button
|
||||||
|
panel.find('#search-close').click(function(event) {
|
||||||
|
// Prevent this button from actually submitting the form
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchInputTimer = null;
|
||||||
|
var searchText = null;
|
||||||
|
var searchTextCurrent = null;
|
||||||
|
var searchQueries = [];
|
||||||
|
|
||||||
|
function searchTextChanged(event) {
|
||||||
|
|
||||||
|
searchText = $('#offcanvas-search').find('#search-input').val();
|
||||||
|
|
||||||
|
clearTimeout(searchInputTimer);
|
||||||
|
searchInputTimer = setTimeout(updateSearch, 250);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function updateSearch() {
|
||||||
|
|
||||||
|
if (searchText == searchTextCurrent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearchResults();
|
||||||
|
|
||||||
|
if (searchText.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTextCurrent = searchText;
|
||||||
|
|
||||||
|
// Cancel any previous AJAX requests
|
||||||
|
searchQueries.forEach(function(query) {
|
||||||
|
query.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
searchQueries = [];
|
||||||
|
|
||||||
|
// Show the "searching" text
|
||||||
|
$('#offcanvas-search').find('#search-pending').show();
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_PREVIEW_SHOW_PARTS) {
|
||||||
|
|
||||||
|
var params = {};
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) {
|
||||||
|
params.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for matching parts
|
||||||
|
addSearchQuery(
|
||||||
|
'part',
|
||||||
|
'{% trans "Parts" %}',
|
||||||
|
'{% url "api-part-list" %}',
|
||||||
|
params,
|
||||||
|
renderPart,
|
||||||
|
{
|
||||||
|
url: '/part',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) {
|
||||||
|
// Search for matching part categories
|
||||||
|
addSearchQuery(
|
||||||
|
'category',
|
||||||
|
'{% trans "Part Categories" %}',
|
||||||
|
'{% url "api-part-category-list" %}',
|
||||||
|
{},
|
||||||
|
renderPartCategory,
|
||||||
|
{
|
||||||
|
url: '/part/category',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_PREVIEW_SHOW_STOCK) {
|
||||||
|
// Search for matching stock items
|
||||||
|
addSearchQuery(
|
||||||
|
'stock',
|
||||||
|
'{% trans "Stock Items" %}',
|
||||||
|
'{% url "api-stock-list" %}',
|
||||||
|
{
|
||||||
|
part_detail: true,
|
||||||
|
location_detail: true,
|
||||||
|
},
|
||||||
|
renderStockItem,
|
||||||
|
{
|
||||||
|
url: '/stock/item',
|
||||||
|
render_location_detail: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_PREVIEW_SHOW_LOCATIONS) {
|
||||||
|
// Search for matching stock locations
|
||||||
|
addSearchQuery(
|
||||||
|
'location',
|
||||||
|
'{% trans "Stock Locations" %}',
|
||||||
|
'{% url "api-location-list" %}',
|
||||||
|
{},
|
||||||
|
renderStockLocation,
|
||||||
|
{
|
||||||
|
url: '/stock/location',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) {
|
||||||
|
// Search for matching companies
|
||||||
|
addSearchQuery(
|
||||||
|
'company',
|
||||||
|
'{% trans "Companies" %}',
|
||||||
|
'{% url "api-company-list" %}',
|
||||||
|
{},
|
||||||
|
renderCompany,
|
||||||
|
{
|
||||||
|
url: '/company',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS) {
|
||||||
|
// Search for matching purchase orders
|
||||||
|
addSearchQuery(
|
||||||
|
'purchaseorder',
|
||||||
|
'{% trans "Purchase Orders" %}',
|
||||||
|
'{% url "api-po-list" %}',
|
||||||
|
{
|
||||||
|
supplier_detail: true,
|
||||||
|
outstanding: true,
|
||||||
|
},
|
||||||
|
renderPurchaseOrder,
|
||||||
|
{
|
||||||
|
url: '/order/purchase-order',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_settings.SEARCH_PREVIEW_SHOW_SALES_ORDERS) {
|
||||||
|
// Search for matching sales orders
|
||||||
|
addSearchQuery(
|
||||||
|
'salesorder',
|
||||||
|
'{% trans "Sales Orders" %}',
|
||||||
|
'{% url "api-so-list" %}',
|
||||||
|
{
|
||||||
|
customer_detail: true,
|
||||||
|
outstanding: true,
|
||||||
|
},
|
||||||
|
renderSalesOrder,
|
||||||
|
{
|
||||||
|
url: '/order/sales-order',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until all the pending queries are completed
|
||||||
|
$.when.apply($, searchQueries).done(function() {
|
||||||
|
$('#offcanvas-search').find('#search-pending').hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clearSearchResults() {
|
||||||
|
|
||||||
|
var panel = $('#offcanvas-search');
|
||||||
|
|
||||||
|
// Ensure the 'no results found' element is visible
|
||||||
|
panel.find('#search-no-results').show();
|
||||||
|
|
||||||
|
// Ensure that the 'searching' element is hidden
|
||||||
|
panel.find('#search-pending').hide();
|
||||||
|
|
||||||
|
// Delete any existing search results
|
||||||
|
panel.find('#search-results').empty();
|
||||||
|
|
||||||
|
// Finally, grab keyboard focus in the search bar
|
||||||
|
panel.find('#search-input').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addSearchQuery(key, title, query_url, query_params, render_func, render_params={}) {
|
||||||
|
|
||||||
|
// Include current search term
|
||||||
|
query_params.search = searchTextCurrent;
|
||||||
|
|
||||||
|
// How many results to show in each group?
|
||||||
|
query_params.offset = 0;
|
||||||
|
query_params.limit = user_settings.SEARCH_PREVIEW_RESULTS;
|
||||||
|
|
||||||
|
// Do not display "pk" value for search results
|
||||||
|
render_params.render_pk = false;
|
||||||
|
|
||||||
|
// Add the result group to the panel
|
||||||
|
$('#offcanvas-search').find('#search-results').append(`
|
||||||
|
<div class='search-result-group-wrapper' id='search-results-wrapper-${key}'></div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
var request = inventreeGet(
|
||||||
|
query_url,
|
||||||
|
query_params,
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
addSearchResults(
|
||||||
|
key,
|
||||||
|
response.results,
|
||||||
|
title,
|
||||||
|
render_func,
|
||||||
|
render_params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the query to the stack
|
||||||
|
searchQueries.push(request);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Add a group of results to the list
|
||||||
|
function addSearchResults(key, results, title, renderFunc, renderParams={}) {
|
||||||
|
|
||||||
|
if (results.length == 0) {
|
||||||
|
// Do not display this group, as there are no results
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var panel = $('#offcanvas-search');
|
||||||
|
|
||||||
|
// Ensure the 'no results found' element is hidden
|
||||||
|
panel.find('#search-no-results').hide();
|
||||||
|
|
||||||
|
panel.find(`#search-results-wrapper-${key}`).append(`
|
||||||
|
<div class='search-result-group' id='search-results-${key}'>
|
||||||
|
<div class='search-result-header' style='display: flex;'>
|
||||||
|
<h5>${title}</h5>
|
||||||
|
<span class='flex' style='flex-grow: 1;'></span>
|
||||||
|
<div class='search-result-group-buttons btn-group float-right' role='group'>
|
||||||
|
<button class='btn btn-outline-secondary' id='hide-results-${key}' title='{% trans "Minimize results" %}'>
|
||||||
|
<span class='fas fa-chevron-up'></span>
|
||||||
|
</button>
|
||||||
|
<button class='btn btn-outline-secondary' id='remove-results-${key}' title='{% trans "Remove results" %}'>
|
||||||
|
<span class='fas fa-times icon-red'></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='collapse search-result-list' id='search-result-list-${key}'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
results.forEach(function(result) {
|
||||||
|
|
||||||
|
var pk = result.pk || result.id;
|
||||||
|
|
||||||
|
var html = renderFunc(key, result, renderParams);
|
||||||
|
|
||||||
|
if (renderParams.url) {
|
||||||
|
html = `<a href='${renderParams.url}/${pk}/'>` + html + `</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result_html = `
|
||||||
|
<div class='search-result-entry' id='search-result-${key}-${pk}'>
|
||||||
|
${html}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
panel.find(`#search-result-list-${key}`).append(result_html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand results panel
|
||||||
|
panel.find(`#search-result-list-${key}`).toggle();
|
||||||
|
|
||||||
|
// Add callback for "toggle" button
|
||||||
|
panel.find(`#hide-results-${key}`).click(function() {
|
||||||
|
panel.find(`#search-result-list-${key}`).toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add callback for "remove" button
|
||||||
|
panel.find(`#remove-results-${key}`).click(function() {
|
||||||
|
panel.find(`#search-results-${key}`).remove();
|
||||||
|
});
|
||||||
|
}
|
@ -427,12 +427,16 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
},
|
},
|
||||||
has_stock: {
|
has_stock: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Stock available" %}',
|
title: '{% trans "In stock" %}',
|
||||||
},
|
},
|
||||||
low_stock: {
|
low_stock: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Low stock" %}',
|
title: '{% trans "Low stock" %}',
|
||||||
},
|
},
|
||||||
|
unallocated_stock: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Available stock" %}',
|
||||||
|
},
|
||||||
assembly: {
|
assembly: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Assembly" %}',
|
title: '{% trans "Assembly" %}',
|
||||||
|
@ -87,18 +87,25 @@
|
|||||||
{% if demo %}
|
{% if demo %}
|
||||||
{% include "navbar_demo.html" %}
|
{% include "navbar_demo.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "search_form.html" %}
|
|
||||||
<ul class='navbar-nav flex-row'>
|
<ul class='navbar-nav flex-row'>
|
||||||
|
|
||||||
|
<li class='nav-item me-2'>
|
||||||
|
<button data-bs-toggle='offcanvas' data-bs-target="#offcanvas-search" class='btn position-relative' title='{% trans "Search" %}'>
|
||||||
|
<span class='fas fa-search'></span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
{% if barcodes %}
|
{% if barcodes %}
|
||||||
<li class='nav-item' id='navbar-barcode-li'>
|
<li class='nav-item' id='navbar-barcode-li'>
|
||||||
<button id='barcode-scan' class='btn btn-secondary' title='{% trans "Scan Barcode" %}'>
|
<button id='barcode-scan' class='btn position-relative' title='{% trans "Scan Barcode" %}'>
|
||||||
<span class='fas fa-qrcode'></span>
|
<span class='fas fa-qrcode'></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<li class='nav-item me-2'>
|
<li class='nav-item me-2'>
|
||||||
<button data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" class='btn position-relative' title='{% trans "Show Notifications" %}'>
|
<button data-bs-toggle="offcanvas" data-bs-target="#offcanvas-notification" class='btn position-relative' title='{% trans "Show Notifications" %}'>
|
||||||
<span class='fas fa-bell'></span>
|
<span class='fas fa-bell'></span>
|
||||||
<span class="position-absolute top-100 start-100 translate-middle badge rounded-pill bg-danger d-none" id="notification-alert">
|
<span class="position-absolute top-100 start-100 translate-middle badge rounded-pill bg-danger d-none" id="notification-alert">
|
||||||
<span class="visually-hidden">{% trans "New Notifications" %}</span>
|
<span class="visually-hidden">{% trans "New Notifications" %}</span>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" data-bs-scroll="true" aria-labelledby="offcanvasRightLabel">
|
|
||||||
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvas-notification" data-bs-scroll="true" aria-labelledby="offcanvas-notification-label">
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
<h5 id="offcanvasRightLabel">{% trans "Notifications" %}</h5>
|
<h5 id="offcanvas-notification-label">{% trans "Notifications" %}</h5>
|
||||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="offcanvas-body">
|
<div class="offcanvas-body">
|
||||||
@ -11,4 +12,5 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<a href="{% url 'notifications' %}">{% trans "Show all notifications and history" %}</a>
|
<a href="{% url 'notifications' %}">{% trans "Show all notifications and history" %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
43
InvenTree/templates/search.html
Normal file
43
InvenTree/templates/search.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="offcanvas offcanvas-end search-result-panel" tabindex="-1" id="offcanvas-search" data-bs-scroll="true" aria-labelledby="offcanvas-search-label">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<form action='{% url "search" %}' method='post' class='d-flex' style='width: 100%;'>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class='input-group'>
|
||||||
|
<input type="text" name='search' class="form-control" aria-label='{% trans "Search" %}' id="search-input" placeholder="{% trans 'Search' %}" autofocus>
|
||||||
|
<button type='submit' id='search-complete' class='btn btn-outline-secondary' title='{% trans "Show full search results" %}'>
|
||||||
|
<span class='fas fa-search'></span>
|
||||||
|
</button>
|
||||||
|
<button id='search-clear' class='btn btn-outline-secondary' title='{% trans "Clear search" %}'>
|
||||||
|
<span class='fas fa-backspace'></span>
|
||||||
|
</button>
|
||||||
|
<!--
|
||||||
|
<button id='search-filter' class="btn btn-outline-secondary" title='{% trans "Filter results" %}'>
|
||||||
|
<span class='fas fa-filter'></span>
|
||||||
|
</button>
|
||||||
|
-->
|
||||||
|
<button id='search-close' class="btn btn-outline-secondary" data-bs-dismiss='offcanvas' title='{% trans "Close search menu" %}'>
|
||||||
|
<span class='fas fa-times icon-red'></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body">
|
||||||
|
<div id="search-center">
|
||||||
|
<p id='search-pending' class='text-muted' display='none'>
|
||||||
|
<em>{% trans "Searching" %}...</em>
|
||||||
|
<span class='float-right'>
|
||||||
|
<span class='fas fa-spinner fa-spin'></span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p id='search-no-results' class='text-muted'>
|
||||||
|
<em>{% trans "No search results" %}</em>
|
||||||
|
</p>
|
||||||
|
<div id='search-results'>
|
||||||
|
<!-- Search results go here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
# Base python requirements for docker containers
|
# Base python requirements for docker containers
|
||||||
|
|
||||||
# Basic package requirements
|
# Basic package requirements
|
||||||
setuptools>=57.4.0,<=60.1.0
|
setuptools==60.0.5
|
||||||
wheel>=0.37.0
|
wheel>=0.37.0
|
||||||
invoke>=1.4.0 # Invoke build tool
|
invoke>=1.4.0 # Invoke build tool
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user