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 {
|
||||
text-decoration: none;
|
||||
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-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
|
||||
$('.brand-icon').each(function(i, obj) {
|
||||
loadBrandIcon($(this), $(this).attr('brand_name'));
|
||||
@ -231,8 +156,13 @@ function inventreeDocReady() {
|
||||
stopNotificationWatcher();
|
||||
});
|
||||
|
||||
$('#offcanvasRight').on('show.bs.offcanvas', openNotificationPanel); // listener for opening the notification panel
|
||||
$('#offcanvasRight').on('hidden.bs.offcanvas', closeNotificationPanel); // listener for closing the notification panel
|
||||
// Calbacks for search 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
|
||||
N_SCRIPT_FILES = 37
|
||||
N_SCRIPT_FILES = 38
|
||||
|
||||
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'^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'^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'^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'),
|
||||
|
@ -12,11 +12,18 @@ import common.models
|
||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
- 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
|
||||
"""
|
||||
|
||||
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!
|
||||
if not latest:
|
||||
|
@ -1,5 +1,7 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block pre_form_content %}
|
||||
Are you sure you want to delete this build?
|
||||
|
||||
{% trans "Are you sure you want to delete this build?" %}
|
||||
|
||||
{% endblock %}
|
@ -1247,6 +1247,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'LABEL_ENABLE': {
|
||||
'name': _('Enable label printing'),
|
||||
'description': _('Enable label printing from the web interface'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
"LABEL_INLINE": {
|
||||
'name': _('Inline label display'),
|
||||
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
|
||||
@ -1261,20 +1268,62 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'SEARCH_PREVIEW_RESULTS': {
|
||||
'name': _('Search Preview Results'),
|
||||
'description': _('Number of results to show 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'),
|
||||
'SEARCH_PREVIEW_SHOW_PARTS': {
|
||||
'name': _('Search Parts'),
|
||||
'description': _('Display parts in search preview window'),
|
||||
'default': True,
|
||||
'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': {
|
||||
'name': _("Hide Inactive Parts"),
|
||||
'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
|
||||
|
||||
# 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()
|
||||
|
||||
assembly = rest_filters.BooleanFilter()
|
||||
@ -1334,6 +1348,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
'creation_date',
|
||||
'IPN',
|
||||
'in_stock',
|
||||
'unallocated_stock',
|
||||
'category',
|
||||
]
|
||||
|
||||
|
@ -38,3 +38,11 @@
|
||||
part: 1
|
||||
sub_part: 5
|
||||
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
|
||||
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
|
||||
- model: part.part
|
||||
pk: 10000
|
||||
|
@ -1345,7 +1345,8 @@ class Part(MPTTModel):
|
||||
|
||||
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:
|
||||
# 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
|
||||
"""
|
||||
|
||||
return self.get_stock_count()
|
||||
return self.get_stock_count(include_variants=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.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.utils.translation import ugettext_lazy as _
|
||||
|
||||
@ -24,7 +24,10 @@ from InvenTree.serializers import (DataFileUploadSerializer,
|
||||
InvenTreeAttachmentSerializer,
|
||||
InvenTreeMoneySerializer)
|
||||
|
||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
||||
from InvenTree.status_codes import (BuildStatus,
|
||||
PurchaseOrderStatus,
|
||||
SalesOrderStatus)
|
||||
|
||||
from stock.models import StockItem
|
||||
|
||||
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
|
||||
|
||||
def get_starred(self, part):
|
||||
@ -376,9 +424,12 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
category_detail = CategorySerializer(source='category', many=False, read_only=True)
|
||||
|
||||
# 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)
|
||||
ordering = serializers.FloatField(read_only=True)
|
||||
building = serializers.FloatField(read_only=True)
|
||||
stock_item_count = serializers.IntegerField(read_only=True)
|
||||
suppliers = serializers.IntegerField(read_only=True)
|
||||
|
||||
@ -399,7 +450,8 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
partial = True
|
||||
fields = [
|
||||
'active',
|
||||
|
||||
'allocated_to_build_orders',
|
||||
'allocated_to_sales_orders',
|
||||
'assembly',
|
||||
'category',
|
||||
'category_detail',
|
||||
@ -430,6 +482,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
'suppliers',
|
||||
'thumbnail',
|
||||
'trackable',
|
||||
'unallocated_stock',
|
||||
'units',
|
||||
'variant_of',
|
||||
'virtual',
|
||||
|
@ -37,13 +37,17 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if barcodes %}
|
||||
{% if barcodes or labels_enabled %}
|
||||
<!-- Barcode actions menu -->
|
||||
<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>
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if labels_enabled %}
|
||||
<li><a class='dropdown-item' href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -424,9 +428,11 @@
|
||||
);
|
||||
});
|
||||
|
||||
{% if labels_enabled %}
|
||||
$('#print-label').click(function() {
|
||||
printPartLabels([{{ part.pk }}]);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
function adjustPartStock(action) {
|
||||
inventreeGet(
|
||||
|
@ -9,7 +9,7 @@ from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
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 BomItem, BomItemSubstitute
|
||||
@ -17,6 +17,9 @@ from stock.models import StockItem, StockLocation
|
||||
from company.models import Company
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
import build.models
|
||||
import order.models
|
||||
|
||||
|
||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||
"""
|
||||
@ -247,7 +250,7 @@ class PartAPITest(InvenTreeAPITestCase):
|
||||
data = {'cascade': True}
|
||||
response = self.client.get(url, data, format='json')
|
||||
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):
|
||||
url = reverse('api-part-list')
|
||||
@ -815,6 +818,10 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
'location',
|
||||
'bom',
|
||||
'test_templates',
|
||||
'build',
|
||||
'location',
|
||||
'stock',
|
||||
'sales_order',
|
||||
]
|
||||
|
||||
roles = [
|
||||
@ -826,6 +833,9 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
|
||||
super().setUp()
|
||||
|
||||
# Ensure the part "variant" tree is correctly structured
|
||||
Part.objects.rebuild()
|
||||
|
||||
# Add a new part
|
||||
self.part = Part.objects.create(
|
||||
name='Banana',
|
||||
@ -880,6 +890,153 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(data['in_stock'], 1100)
|
||||
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):
|
||||
"""
|
||||
|
@ -46,7 +46,7 @@ class BomItemTest(TestCase):
|
||||
# TODO: Tests for multi-level BOMs
|
||||
|
||||
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)
|
||||
|
||||
def test_self_reference(self):
|
||||
|
@ -251,3 +251,104 @@
|
||||
rght: 0
|
||||
expiry_date: "1990-10-10"
|
||||
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>
|
||||
{% endif %}
|
||||
<!-- Document / label menu -->
|
||||
{% if test_report_enabled or labels_enabled %}
|
||||
<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>
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Stock adjustment menu -->
|
||||
{% if user_owns_item %}
|
||||
{% 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>
|
||||
<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>
|
||||
{% if labels_enabled %}
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -181,6 +183,7 @@
|
||||
<div id='sublocation-button-toolbar'>
|
||||
<div class='btn-group' role='group'>
|
||||
<!-- Printing actions menu -->
|
||||
{% if labels_enabled %}
|
||||
<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" %}'>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "filter_list.html" with id="location" %}
|
||||
</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() {
|
||||
|
||||
var selections = $('#sublocation-table').bootstrapTable('getSelections');
|
||||
@ -234,6 +247,7 @@
|
||||
|
||||
printStockLocationLabels(locations);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if location %}
|
||||
$("#barcode-check-in").click(function() {
|
||||
@ -298,14 +312,6 @@
|
||||
adjustLocationStock('move');
|
||||
});
|
||||
|
||||
$('#print-label').click(function() {
|
||||
|
||||
var locs = [{{ location.pk }}];
|
||||
|
||||
printStockLocationLabels(locs);
|
||||
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
$('#show-qr-code').click(function() {
|
||||
|
@ -104,7 +104,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
|
||||
response = self.get_stock()
|
||||
|
||||
self.assertEqual(len(response), 20)
|
||||
self.assertEqual(len(response), 29)
|
||||
|
||||
def test_filter_by_part(self):
|
||||
"""
|
||||
@ -113,7 +113,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
|
||||
response = self.get_stock(part=25)
|
||||
|
||||
self.assertEqual(len(response), 8)
|
||||
self.assertEqual(len(response), 17)
|
||||
|
||||
response = self.get_stock(part=10004)
|
||||
|
||||
@ -136,13 +136,13 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
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)
|
||||
self.assertEqual(len(response), 2)
|
||||
self.assertEqual(len(response), 9)
|
||||
|
||||
response = self.get_stock(location=7)
|
||||
self.assertEqual(len(response), 16)
|
||||
self.assertEqual(len(response), 18)
|
||||
|
||||
def test_filter_by_depleted(self):
|
||||
"""
|
||||
@ -153,7 +153,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
response = self.get_stock(depleted=0)
|
||||
self.assertEqual(len(response), 19)
|
||||
self.assertEqual(len(response), 28)
|
||||
|
||||
def test_filter_by_in_stock(self):
|
||||
"""
|
||||
@ -161,7 +161,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
"""
|
||||
|
||||
response = self.get_stock(in_stock=1)
|
||||
self.assertEqual(len(response), 17)
|
||||
self.assertEqual(len(response), 26)
|
||||
|
||||
response = self.get_stock(in_stock=0)
|
||||
self.assertEqual(len(response), 3)
|
||||
@ -172,7 +172,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
"""
|
||||
|
||||
codes = {
|
||||
StockStatus.OK: 18,
|
||||
StockStatus.OK: 27,
|
||||
StockStatus.DESTROYED: 1,
|
||||
StockStatus.LOST: 1,
|
||||
StockStatus.DAMAGED: 0,
|
||||
@ -205,7 +205,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertIsNotNone(item['serial'])
|
||||
|
||||
response = self.get_stock(serialized=0)
|
||||
self.assertEqual(len(response), 8)
|
||||
self.assertEqual(len(response), 17)
|
||||
|
||||
for item in response:
|
||||
self.assertIsNone(item['serial'])
|
||||
@ -217,7 +217,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
|
||||
# First, we can assume that the 'stock expiry' feature is disabled
|
||||
response = self.get_stock(expired=1)
|
||||
self.assertEqual(len(response), 20)
|
||||
self.assertEqual(len(response), 29)
|
||||
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
@ -232,7 +232,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertTrue(item['expired'])
|
||||
|
||||
response = self.get_stock(expired=0)
|
||||
self.assertEqual(len(response), 19)
|
||||
self.assertEqual(len(response), 28)
|
||||
|
||||
for item in response:
|
||||
self.assertFalse(item['expired'])
|
||||
@ -249,7 +249,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 4)
|
||||
|
||||
response = self.get_stock(expired=0)
|
||||
self.assertEqual(len(response), 16)
|
||||
self.assertEqual(len(response), 25)
|
||||
|
||||
def test_paginate(self):
|
||||
"""
|
||||
@ -290,7 +290,8 @@ class StockItemListTest(StockAPITestCase):
|
||||
|
||||
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
|
||||
headers = [
|
||||
@ -308,11 +309,11 @@ class StockItemListTest(StockAPITestCase):
|
||||
# Now, add a filter to the results
|
||||
dataset = self.export_data({'location': 1})
|
||||
|
||||
self.assertEqual(len(dataset), 2)
|
||||
self.assertEqual(len(dataset), 9)
|
||||
|
||||
dataset = self.export_data({'part': 25})
|
||||
|
||||
self.assertEqual(len(dataset), 8)
|
||||
self.assertEqual(len(dataset), 17)
|
||||
|
||||
|
||||
class StockItemTest(StockAPITestCase):
|
||||
|
@ -167,8 +167,8 @@ class StockTest(TestCase):
|
||||
self.assertFalse(self.drawer2.has_items())
|
||||
|
||||
# Drawer 3 should have three stock items
|
||||
self.assertEqual(self.drawer3.stock_items.count(), 16)
|
||||
self.assertEqual(self.drawer3.item_count, 16)
|
||||
self.assertEqual(self.drawer3.stock_items.count(), 18)
|
||||
self.assertEqual(self.drawer3.item_count, 18)
|
||||
|
||||
def test_stock_count(self):
|
||||
part = Part.objects.get(pk=1)
|
||||
|
@ -14,6 +14,7 @@
|
||||
<div class='row'>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -14,8 +14,16 @@
|
||||
<div class='row'>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<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_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' %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -6,6 +6,7 @@
|
||||
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
|
||||
{% settings_value "REPORT_ENABLE" as report_enabled %}
|
||||
{% 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 %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -126,9 +127,11 @@
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{% include 'modals.html' %}
|
||||
{% include 'about.html' %}
|
||||
{% include "notifications.html" %}
|
||||
{% include "search.html" %}
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
@ -185,6 +188,7 @@
|
||||
<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 '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 'plugin.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
|
||||
|
@ -4,6 +4,7 @@
|
||||
editSetting,
|
||||
user_settings,
|
||||
global_settings,
|
||||
plugins_enabled,
|
||||
*/
|
||||
|
||||
{% user_settings request.user as USER_SETTINGS %}
|
||||
@ -20,6 +21,13 @@ const global_settings = {
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
{% plugins_enabled as p_en %}
|
||||
{% if p_en %}
|
||||
const plugins_enabled = true;
|
||||
{% else %}
|
||||
const plugins_enabled = false;
|
||||
{% endif %}
|
||||
|
||||
/*
|
||||
* Edit a setting value
|
||||
*/
|
||||
|
@ -1746,7 +1746,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
required: true,
|
||||
render_part_detail: true,
|
||||
render_location_detail: true,
|
||||
render_stock_id: false,
|
||||
render_pk: false,
|
||||
auto_fill: true,
|
||||
auto_fill_filters: auto_fill_filters,
|
||||
onSelect: function(data, field, opts) {
|
||||
|
@ -10,6 +10,7 @@
|
||||
modalSetTitle,
|
||||
modalSubmit,
|
||||
openModal,
|
||||
plugins_enabled,
|
||||
showAlertDialog,
|
||||
*/
|
||||
|
||||
@ -232,26 +233,28 @@ function selectLabel(labels, items, options={}) {
|
||||
var plugins = [];
|
||||
|
||||
// Request a list of available label printing plugins from the server
|
||||
inventreeGet(
|
||||
`/api/plugin/`,
|
||||
{},
|
||||
{
|
||||
async: false,
|
||||
success: function(response) {
|
||||
response.forEach(function(plugin) {
|
||||
// Look for active plugins which implement the 'labels' mixin class
|
||||
if (plugin.active && plugin.mixins && plugin.mixins.labels) {
|
||||
// This plugin supports label printing
|
||||
plugins.push(plugin);
|
||||
}
|
||||
});
|
||||
if (plugins_enabled) {
|
||||
inventreeGet(
|
||||
`/api/plugin/`,
|
||||
{},
|
||||
{
|
||||
async: false,
|
||||
success: function(response) {
|
||||
response.forEach(function(plugin) {
|
||||
// Look for active plugins which implement the 'labels' mixin class
|
||||
if (plugin.active && plugin.mixins && plugin.mixins.labels) {
|
||||
// This plugin supports label printing
|
||||
plugins.push(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
var plugin_selection = '';
|
||||
|
||||
if (plugins.length > 0) {
|
||||
if (plugins_enabled && plugins.length > 0) {
|
||||
plugin_selection =`
|
||||
<div class='form-group'>
|
||||
<label class='control-label requiredField' for='id_plugin'>
|
||||
|
@ -10,7 +10,9 @@
|
||||
renderCompany,
|
||||
renderManufacturerPart,
|
||||
renderOwner,
|
||||
renderPart,
|
||||
renderPartCategory,
|
||||
renderStockItem,
|
||||
renderStockLocation,
|
||||
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
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderCompany(name, data, parameters, options) {
|
||||
function renderCompany(name, data, parameters={}, options={}) {
|
||||
|
||||
var html = select2Thumbnail(data.image);
|
||||
|
||||
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;
|
||||
}
|
||||
@ -45,7 +65,7 @@ function renderCompany(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "StockItem" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderStockItem(name, data, parameters, options) {
|
||||
function renderStockItem(name, data, parameters={}, options={}) {
|
||||
|
||||
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> - `;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if ('render_location_detail' in parameters) {
|
||||
@ -86,7 +94,7 @@ function renderStockItem(name, data, parameters, options) {
|
||||
var 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 = '';
|
||||
@ -101,7 +109,10 @@ function renderStockItem(name, data, parameters, options) {
|
||||
|
||||
var html = `
|
||||
<span>
|
||||
${part_detail}${stock_detail}${location_detail}${stock_id}
|
||||
${part_detail}
|
||||
${stock_detail}
|
||||
${location_detail}
|
||||
${renderId('{% trans "Stock ID" %}', data.pk, parameters)}
|
||||
</span>
|
||||
`;
|
||||
|
||||
@ -111,7 +122,7 @@ function renderStockItem(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "StockLocation" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderStockLocation(name, data, parameters, options) {
|
||||
function renderStockLocation(name, data, parameters={}, options={}) {
|
||||
|
||||
var level = '- '.repeat(data.level);
|
||||
|
||||
@ -133,7 +144,7 @@ function renderStockLocation(name, data, parameters, options) {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderBuild(name, data, parameters, options) {
|
||||
function renderBuild(name, data, parameters={}, options={}) {
|
||||
|
||||
var image = null;
|
||||
|
||||
@ -154,7 +165,7 @@ function renderBuild(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "Part" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderPart(name, data, parameters, options) {
|
||||
function renderPart(name, data, parameters={}, options={}) {
|
||||
|
||||
var html = select2Thumbnail(data.image);
|
||||
|
||||
@ -164,13 +175,14 @@ function renderPart(name, data, parameters, options) {
|
||||
html += ` - <i><small>${data.description}</small></i>`;
|
||||
}
|
||||
|
||||
var extra = '';
|
||||
var stock_data = '';
|
||||
|
||||
// Display available part quantity
|
||||
if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) {
|
||||
extra += partStockLabel(data);
|
||||
stock_data = partStockLabel(data);
|
||||
}
|
||||
|
||||
var extra = '';
|
||||
|
||||
if (!data.active) {
|
||||
extra += `<span class='badge badge-right rounded-pill bg-danger'>{% trans "Inactive" %}</span>`;
|
||||
}
|
||||
@ -178,8 +190,9 @@ function renderPart(name, data, parameters, options) {
|
||||
html += `
|
||||
<span class='float-right'>
|
||||
<small>
|
||||
${stock_data}
|
||||
${extra}
|
||||
{% trans "Part ID" %}: ${data.pk}
|
||||
${renderId('{% trans "Part ID" $}', data.pk, parameters)}
|
||||
</small>
|
||||
</span>`;
|
||||
|
||||
@ -188,7 +201,7 @@ function renderPart(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "User" model
|
||||
// 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>`;
|
||||
|
||||
@ -202,7 +215,7 @@ function renderUser(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "Owner" model
|
||||
// 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>`;
|
||||
|
||||
@ -223,15 +236,13 @@ function renderOwner(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "PurchaseOrder" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderPurchaseOrder(name, data, parameters, options) {
|
||||
var html = '';
|
||||
function renderPurchaseOrder(name, data, parameters={}, options={}) {
|
||||
|
||||
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
|
||||
var html = `<span>${prefix}${data.reference}</span>`;
|
||||
|
||||
var thumbnail = null;
|
||||
|
||||
html += `<span>${prefix}${data.reference}</span>`;
|
||||
|
||||
if (data.supplier_detail) {
|
||||
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 += `
|
||||
<span class='float-right'>
|
||||
<small>
|
||||
{% trans "Order ID" %}: ${data.pk}
|
||||
</small>
|
||||
</span>
|
||||
`;
|
||||
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -257,19 +262,25 @@ function renderPurchaseOrder(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "SalesOrder" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderSalesOrder(name, data, parameters, options) {
|
||||
var html = `<span>${data.reference}</span>`;
|
||||
function renderSalesOrder(name, data, parameters={}, options={}) {
|
||||
|
||||
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) {
|
||||
html += ` - <em>${data.description}</em>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<span class='float-right'>
|
||||
<small>
|
||||
{% trans "Order ID" %}: ${data.pk}
|
||||
</small>
|
||||
</span>`;
|
||||
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -277,7 +288,7 @@ function renderSalesOrder(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "SalesOrderShipment" model
|
||||
// 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;
|
||||
|
||||
@ -294,7 +305,7 @@ function renderSalesOrderShipment(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "PartCategory" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderPartCategory(name, data, parameters, options) {
|
||||
function renderPartCategory(name, data, parameters={}, options={}) {
|
||||
|
||||
var level = '- '.repeat(data.level);
|
||||
|
||||
@ -310,7 +321,7 @@ function renderPartCategory(name, data, parameters, options) {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderPartParameterTemplate(name, data, parameters, options) {
|
||||
function renderPartParameterTemplate(name, data, parameters={}, options={}) {
|
||||
|
||||
var units = '';
|
||||
|
||||
@ -326,7 +337,7 @@ function renderPartParameterTemplate(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "ManufacturerPart" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderManufacturerPart(name, data, parameters, options) {
|
||||
function renderManufacturerPart(name, data, parameters={}, options={}) {
|
||||
|
||||
var manufacturer_image = null;
|
||||
var part_image = null;
|
||||
@ -355,7 +366,7 @@ function renderManufacturerPart(name, data, parameters, options) {
|
||||
|
||||
// Renderer for "SupplierPart" model
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderSupplierPart(name, data, parameters, options) {
|
||||
function renderSupplierPart(name, data, parameters={}, options={}) {
|
||||
|
||||
var supplier_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={}) {
|
||||
|
||||
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 {
|
||||
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) {
|
||||
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>`;
|
||||
|
||||
if (part.on_order) {
|
||||
rows += `<tr><td><b>{$ trans "On Order" %}</b></td><td>${part.on_order}</td></tr>`;
|
||||
if (part.ordering) {
|
||||
rows += `<tr><td><b>{% trans "On Order" %}</b></td><td>${part.ordering}</td></tr>`;
|
||||
}
|
||||
|
||||
if (part.building) {
|
||||
@ -1322,31 +1361,47 @@ function loadPartTable(table, url, options={}) {
|
||||
columns.push(col);
|
||||
|
||||
col = {
|
||||
field: 'in_stock',
|
||||
field: 'unallocated_stock',
|
||||
title: '{% trans "Stock" %}',
|
||||
searchable: false,
|
||||
formatter: function(value, row) {
|
||||
var link = '?display=part-stock';
|
||||
|
||||
if (value) {
|
||||
if (row.in_stock) {
|
||||
// There IS stock available for this part
|
||||
|
||||
// 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>`;
|
||||
} 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 {
|
||||
// There is no stock available
|
||||
value = `0<span class='badge badge-right rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
||||
// There IS NO stock available for this part
|
||||
|
||||
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}`);
|
||||
|
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: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Stock available" %}',
|
||||
title: '{% trans "In stock" %}',
|
||||
},
|
||||
low_stock: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Low stock" %}',
|
||||
},
|
||||
unallocated_stock: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Available stock" %}',
|
||||
},
|
||||
assembly: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Assembly" %}',
|
||||
|
@ -87,18 +87,25 @@
|
||||
{% if demo %}
|
||||
{% include "navbar_demo.html" %}
|
||||
{% endif %}
|
||||
{% include "search_form.html" %}
|
||||
|
||||
<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 %}
|
||||
<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>
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<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="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>
|
||||
|
@ -1,7 +1,8 @@
|
||||
{% 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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
@ -11,4 +12,5 @@
|
||||
<hr>
|
||||
<a href="{% url 'notifications' %}">{% trans "Show all notifications and history" %}</a>
|
||||
</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
|
||||
|
||||
# Basic package requirements
|
||||
setuptools>=57.4.0,<=60.1.0
|
||||
setuptools==60.0.5
|
||||
wheel>=0.37.0
|
||||
invoke>=1.4.0 # Invoke build tool
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user