diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index f3fd9ef306..0e1622a49a 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -7,9 +7,56 @@ from django.utils.translation import gettext_lazy as _ def validate_part_name(value): - # Prevent some illegal characters in part names - for c in ['|', '#', '$']: + """ Prevent some illegal characters in part names. + """ + + for c in ['|', '#', '$', '{', '}']: if c in str(value): raise ValidationError( _('Invalid character in part name') ) + + +def validate_overage(value): + """ Validate that a BOM overage string is properly formatted. + + An overage string can look like: + + - An integer number ('1' / 3 / 4) + - A percentage ('5%' / '10 %') + """ + + value = str(value).lower().strip() + + # First look for a simple integer value + try: + i = int(value) + + if i < 0: + raise ValidationError(_("Overage value must not be negative")) + + # Looks like an integer! + return True + except ValueError: + pass + + # Now look for a percentage value + if value.endswith('%'): + v = value[:-1].strip() + + # Does it look like a number? + try: + f = float(v) + + if f < 0: + raise ValidationError(_("Overage value must not be negative")) + elif f > 100: + raise ValidationError(_("Overage must not exceed 100%")) + + return True + except ValueError: + pass + + raise ValidationError( + _("Overage must be an integer value or a percentage") + ) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 823a19ba61..c69857fda3 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -59,7 +59,7 @@ class Build(models.Model): take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL, related_name='sourcing_builds', null=True, blank=True, - help_text='Select location to take stock from for this build (leave blank to take from any stock location' + help_text='Select location to take stock from for this build (leave blank to take from any stock location)' ) quantity = models.PositiveIntegerField( @@ -261,7 +261,7 @@ class Build(models.Model): try: item = BomItem.objects.get(part=self.part.id, sub_part=part.id) - return item.quantity * self.quantity + return item.get_required_quantity(self.quantity) except BomItem.DoesNotExist: return 0 diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 2e6d3c694c..63fcf4add8 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals import os +from django.apps import apps from django.db import models from django.urls import reverse from django.conf import settings @@ -111,6 +112,18 @@ class Company(models.Model): """ Return True if this company supplies any parts """ return self.part_count > 0 + @property + def stock_items(self): + """ Return a list of all stock items supplied by this company """ + stock = apps.get_model('stock', 'StockItem') + return stock.objects.filter(supplier_part__supplier=self.id).all() + + @property + def stock_count(self): + """ Return the number of stock items supplied by this company """ + stock = apps.get_model('stock', 'StockItem') + return stock.objects.filter(supplier_part__supplier=self.id).count() + class Contact(models.Model): """ A Contact represents a person who works at a particular company. diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 88792f7f64..2967dbebd5 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -25,7 +25,25 @@ class CompanySerializer(serializers.ModelSerializer): """ Serializer for Company object (full detail) """ url = serializers.CharField(source='get_absolute_url', read_only=True) + part_count = serializers.CharField(read_only=True) class Meta: model = Company - fields = '__all__' + fields = [ + 'id', + 'url', + 'name', + 'description', + 'website', + 'name', + 'phone', + 'address', + 'email', + 'contact', + 'URL', + 'image', + 'notes', + 'is_customer', + 'is_supplier', + 'part_count' + ] diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 56983784ee..8c64190865 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -72,6 +72,11 @@ InvenTree | Company - {{ company.name }} {% endblock %} +{% block js_load %} +{{ block.super }} + +{% endblock %} + {% block js_ready %} enableDragAndDrop( diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index 5d01f1b10e..b3213d5993 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -33,6 +33,14 @@ supplier: {{ company.id }} }, reload: true, + secondary: [ + { + field: 'part', + label: 'New Part', + title: 'Create New Part', + url: "{% url 'part-create' %}" + }, + ] }); }); diff --git a/InvenTree/company/templates/company/detail_stock.html b/InvenTree/company/templates/company/detail_stock.html new file mode 100644 index 0000000000..5bc7b9598a --- /dev/null +++ b/InvenTree/company/templates/company/detail_stock.html @@ -0,0 +1,26 @@ +{% extends "company/company_base.html" %} +{% load static %} + +{% block details %} + +{% include "company/tabs.html" with tab='stock' %} + +

Supplier Stock

+ +{% include "stock_table.html" %} + +{% endblock %} +{% block js_ready %} +{{ block.super }} + + loadStockTable($('#stock-table'), { + url: "{% url 'api-stock-list' %}", + params: { + supplier: {{ company.id }}, + }, + buttons: [ + '#stock-options', + ] + }); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 3f0dc7c489..cea452fdc1 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -3,19 +3,19 @@ {% load static %} {% block page_title %} -InvenTree | Company List +InvenTree | Supplier List {% endblock %} {% block content %}
-

Company List

+

Supplier List

- +
@@ -54,7 +54,7 @@ InvenTree | Company List }, { field: 'name', - title: 'Company', + title: 'Supplier', sortable: true, formatter: function(value, row, index, field) { return imageHoverIcon(row.image) + renderLink(value, row.url); @@ -73,7 +73,14 @@ InvenTree | Company List } return ''; } - } + }, + { + field: 'part_count', + title: 'Parts', + formatter: function(value, row, index, field) { + return renderLink(value, row.url + 'parts/'); + } + }, ], url: "{% url 'api-company-list' %}" }); diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html index df8b9a4fc9..211b56e1d1 100644 --- a/InvenTree/company/templates/company/tabs.html +++ b/InvenTree/company/templates/company/tabs.html @@ -6,6 +6,9 @@ Supplier Parts {{ company.part_count }} + + Stock {{ company.stock_count }} + {% if 0 %} Purchase Orders diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 2c5018ebec..0f8719fe01 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -16,6 +16,7 @@ company_detail_urls = [ # url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'), url(r'parts/?', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'), + url(r'stock/?', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'), url(r'thumbnail/?', views.CompanyImage.as_view(), name='company-image'), diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index e671b49e4f..0973138b21 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -12,7 +12,6 @@ from rest_framework.response import Response from rest_framework import filters from rest_framework import generics, permissions -from django.db.models import Q from django.conf.urls import url, include from django.urls import reverse @@ -109,20 +108,7 @@ class PartList(generics.ListCreateAPIView): if cat_id: try: category = PartCategory.objects.get(pk=cat_id) - - # Filter by the supplied category - flt = Q(category=cat_id) - - if self.request.query_params.get('include_child_categories', None): - childs = category.getUniqueChildren() - for child in childs: - # Ignore the top-level category (already filtered) - if str(child) == str(cat_id): - continue - flt |= Q(category=child) - - parts_list = parts_list.filter(flt) - + parts_list = parts_list.filter(category__in=category.getUniqueChildren()) except PartCategory.DoesNotExist: pass diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 88c6c11385..d4e70ee47a 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -133,6 +133,7 @@ class EditBomItemForm(HelperForm): 'part', 'sub_part', 'quantity', + 'overage', 'note' ] diff --git a/InvenTree/part/migrations/0025_auto_20190515_0012.py b/InvenTree/part/migrations/0025_auto_20190515_0012.py new file mode 100644 index 0000000000..aaeb8ea1a3 --- /dev/null +++ b/InvenTree/part/migrations/0025_auto_20190515_0012.py @@ -0,0 +1,46 @@ +# Generated by Django 2.2 on 2019-05-14 14:12 + +import InvenTree.validators +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0024_partcategory_default_keywords'), + ] + + operations = [ + migrations.AddField( + model_name='bomitem', + name='overage', + field=models.CharField(blank=True, help_text='Estimated build wastage quantity (absolute or percentage)', max_length=24, validators=[InvenTree.validators.validate_overage]), + ), + migrations.AlterField( + model_name='bomitem', + name='note', + field=models.CharField(blank=True, help_text='BOM item notes', max_length=100), + ), + migrations.AlterField( + model_name='bomitem', + name='part', + field=models.ForeignKey(help_text='Select parent part', limit_choices_to={'active': True, 'buildable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'), + ), + migrations.AlterField( + model_name='bomitem', + name='quantity', + field=models.PositiveIntegerField(default=1, help_text='BOM quantity for this BOM item', validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AlterField( + model_name='bomitem', + name='sub_part', + field=models.ForeignKey(help_text='Select part to be used in BOM', limit_choices_to={'active': True, 'consumable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'), + ), + migrations.AlterField( + model_name='supplierpart', + name='URL', + field=models.URLField(blank=True, help_text='URL for external supplier part link'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d255a319f3..f4673dc309 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -300,6 +300,23 @@ class Part(models.Model): # Default case - no default category found return None + def get_default_supplier(self): + """ Get the default supplier part for this part (may be None). + + - If the part specifies a default_supplier, return that + - If there is only one supplier part available, return that + - Else, return None + """ + + if self.default_supplier: + return self.default_suppliers + + if self.supplier_count == 1: + return self.supplier_parts.first() + + # Default to None if there are multiple suppliers to choose from + return None + default_supplier = models.ForeignKey('part.SupplierPart', on_delete=models.SET_NULL, blank=True, null=True, @@ -557,10 +574,11 @@ class Part(models.Model): # Copy the part image if kwargs.get('image', True): - image_file = ContentFile(other.image.read()) - image_file.name = rename_part_image(self, 'test.png') + if other.image: + image_file = ContentFile(other.image.read()) + image_file.name = rename_part_image(self, 'test.png') - self.image = image_file + self.image = image_file # Copy the BOM data if kwargs.get('bom', False): @@ -661,6 +679,7 @@ class BomItem(models.Model): part: Link to the parent part (the part that will be produced) sub_part: Link to the child part (the part that will be consumed) quantity: Number of 'sub_parts' consumed to produce one 'part' + overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%') note: Note field for this BOM item """ @@ -688,6 +707,10 @@ class BomItem(models.Model): # Quantity required quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)], help_text='BOM quantity for this BOM item') + overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage], + help_text='Estimated build wastage quantity (absolute or percentage)' + ) + # Note attached to this BOM line item note = models.CharField(max_length=100, blank=True, help_text='BOM item notes') @@ -721,6 +744,62 @@ class BomItem(models.Model): child=self.sub_part.full_name, n=self.quantity) + def get_overage_quantity(self, quantity): + """ Calculate overage quantity + """ + + # Most of the time overage string will be empty + if len(self.overage) == 0: + return 0 + + overage = str(self.overage).strip() + + # Is the overage an integer value? + try: + ovg = int(overage) + + if ovg < 0: + ovg = 0 + + return ovg + except ValueError: + pass + + # Is the overage a percentage? + if overage.endswith('%'): + overage = overage[:-1].strip() + + try: + percent = float(overage) / 100.0 + if percent > 1: + percent = 1 + if percent < 0: + percent = 0 + + return int(percent * quantity) + + except ValueError: + pass + + # Default = No overage + return 0 + + def get_required_quantity(self, build_quantity): + """ Calculate the required part quantity, based on the supplier build_quantity. + Includes overage estimate in the returned value. + + Args: + build_quantity: Number of parts to build + + Returns: + Quantity required for this build (including overage) + """ + + # Base quantity requirement + base_quantity = self.quantity * build_quantity + + return base_quantity + self.get_overage_quantity(base_quantity) + class SupplierPart(models.Model): """ Represents a unique part as provided by a Supplier diff --git a/InvenTree/part/param_todo.py_todo b/InvenTree/part/param_todo.py_todo deleted file mode 100644 index e597bedf57..0000000000 --- a/InvenTree/part/param_todo.py_todo +++ /dev/null @@ -1,89 +0,0 @@ -""" -TODO - Implement part parameters, and templates - -See code below -""" - - - -class PartParameterTemplate(models.Model): - """ A PartParameterTemplate pre-defines a parameter field, - ready to be copied for use with a given Part. - A PartParameterTemplate can be optionally associated with a PartCategory - """ - name = models.CharField(max_length=20, unique=True) - units = models.CharField(max_length=10, blank=True) - - # Parameter format - PARAM_NUMERIC = 10 - PARAM_TEXT = 20 - PARAM_BOOL = 30 - - PARAM_TYPE_CODES = { - PARAM_NUMERIC: _("Numeric"), - PARAM_TEXT: _("Text"), - PARAM_BOOL: _("Bool") - } - - format = models.PositiveIntegerField( - default=PARAM_NUMERIC, - choices=PARAM_TYPE_CODES.items(), - validators=[MinValueValidator(0)]) - - def __str__(self): - return "{name} ({units})".format( - name=self.name, - units=self.units) - - class Meta: - verbose_name = "Parameter Template" - verbose_name_plural = "Parameter Templates" - - -class CategoryParameterLink(models.Model): - """ Links a PartParameterTemplate to a PartCategory - """ - category = models.ForeignKey(PartCategory, on_delete=models.CASCADE) - template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE) - - def __str__(self): - return "{name} - {cat}".format( - name=self.template.name, - cat=self.category) - - class Meta: - verbose_name = "Category Parameter" - verbose_name_plural = "Category Parameters" - unique_together = ('category', 'template') - - -class PartParameter(models.Model): - """ PartParameter is associated with a single part - """ - - part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') - template = models.ForeignKey(PartParameterTemplate) - - # Value data - value = models.CharField(max_length=50, blank=True) - min_value = models.CharField(max_length=50, blank=True) - max_value = models.CharField(max_length=50, blank=True) - - def __str__(self): - return "{name} : {val}{units}".format( - name=self.template.name, - val=self.value, - units=self.template.units) - - @property - def units(self): - return self.template.units - - @property - def name(self): - return self.template.name - - class Meta: - verbose_name = "Part Parameter" - verbose_name_plural = "Part Parameters" - unique_together = ('part', 'template') diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 37ccb639a0..87ca59c13b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -104,8 +104,6 @@ class PartStarSerializer(InvenTreeModelSerializer): class BomItemSerializer(InvenTreeModelSerializer): """ Serializer for BomItem object """ - # url = serializers.CharField(source='get_absolute_url', read_only=True) - part_detail = PartBriefSerializer(source='part', many=False, read_only=True) sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True) @@ -113,12 +111,12 @@ class BomItemSerializer(InvenTreeModelSerializer): model = BomItem fields = [ 'pk', - # 'url', 'part', 'part_detail', 'sub_part', 'sub_part_detail', 'quantity', + 'overage', 'note', ] diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 3abef26af9..fd8f20d2aa 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -73,8 +73,22 @@ {% if category %} data: { category: {{ category.id }} - } + }, {% endif %} + secondary: [ + { + field: 'default_location', + label: 'New Location', + title: 'Create new location', + url: "{% url 'stock-location-create' %}", + }, + { + field: 'parent', + label: 'New Category', + title: 'Create new category', + url: "{% url 'category-create' %}", + }, + ] }); }) @@ -139,7 +153,6 @@ query: { {% if category %} category: {{ category.id }}, - include_child_categories: true, {% endif %} }, buttons: ['#part-options'], diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html index 3311dafb8d..8b9561ea2e 100644 --- a/InvenTree/part/templates/part/stock.html +++ b/InvenTree/part/templates/part/stock.html @@ -13,25 +13,7 @@

-
- {% if part.active %} - - {% endif %} - -
- - -
- +{% include "stock_table.html" %} {% endblock %} @@ -62,43 +44,4 @@ url: "{% url 'api-stock-list' %}", }); - function selectedStock() { - return $("#stock-table").bootstrapTable('getSelections'); - } - - $("#multi-item-move").click(function() { - - var items = selectedStock(); - - moveStockItems(items, - { - success: function() { - $("#stock-table").bootstrapTable('refresh'); - } - }); - - return false; - }); - - $("#multi-item-stocktake").click(function() { - updateStockItems({ - action: 'stocktake' - }); - return false; - }); - - $("#multi-item-take").click(function() { - updateStockItems({ - action: 'remove', - }); - return false; - }); - - $("#multi-item-give").click(function() { - updateStockItems({ - action: 'add', - }); - return false; - }) - {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index b8d9b7ba6d..15c6f58dcd 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -105,13 +105,6 @@ class PartAPITest(APITestCase): url = reverse('api-part-list') data = {'category': 1} - response = self.client.get(url, data, format='json') - - # There should be 1 part in this category - self.assertEqual(len(response.data), 0) - - data['include_child_categories'] = 1 - # Now request to include child categories response = self.client.get(url, data, format='json') diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 46bdc249ef..6ff81de4fc 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -113,6 +113,15 @@ function loadBomTable(table, options) { title: 'Required', searchable: false, sortable: true, + formatter: function(value, row, index, field) { + var text = value; + + if (row.overage) { + text += " (+" + row.overage + ") "; + } + + return text; + } } ); diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 9f9c5fd40b..d34ed9beee 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -43,6 +43,7 @@ function updateStock(items, options={}) { html += 'Item'; html += 'Location'; html += 'Quantity'; + html += '' + options.action + ''; html += ''; @@ -71,6 +72,9 @@ function updateStock(items, options={}) { } else { html += 'No location set'; } + + html += '' + item.quantity + ''; + html += " +
+ +

Confirm stock count

+
`; - html += "

Note field must be filled

"; var title = ''; @@ -109,6 +123,7 @@ function updateStock(items, options={}) { }); $(modal).find('#note-warning').hide(); + $(modal).find('#confirm-warning').hide(); modalEnable(modal, true); @@ -116,13 +131,23 @@ function updateStock(items, options={}) { var stocktake = []; var notes = $(modal).find('#stocktake-notes').val(); + var confirm = $(modal).find('#stocktake-confirm').is(':checked'); + + var valid = true; if (!notes) { $(modal).find('#note-warning').show(); - return false; + valid = false; } - var valid = true; + if (!confirm) { + $(modal).find('#confirm-warning').show(); + valid = false; + } + + if (!valid) { + return false; + } // Form stocktake data for (idx = 0; idx < items.length; idx++) { @@ -413,6 +438,42 @@ function loadStockTable(table, options) { if (options.buttons) { linkButtonsToSelection(table, options.buttons); } + + // Automatically link button callbacks + $('#multi-item-stocktake').click(function() { + updateStockItems({ + action: 'stocktake', + }); + return false; + }); + + $('#multi-item-remove').click(function() { + updateStockItems({ + action: 'remove', + }); + return false; + }); + + $('#multi-item-add').click(function() { + updateStockItems({ + action: 'add', + }); + return false; + }); + + $("#multi-item-move").click(function() { + + var items = $("#stock-table").bootstrapTable('getSelections'); + + moveStockItems(items, + { + success: function() { + $("#stock-table").bootstrapTable('refresh'); + } + }); + + return false; + }); } diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 9f305b657a..3c08ff8822 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -7,11 +7,12 @@ from django_filters import NumberFilter from django.conf.urls import url, include from django.urls import reverse -from django.db.models import Q from .models import StockLocation, StockItem from .models import StockItemTracking +from part.models import PartCategory + from .serializers import StockItemSerializer, StockQuantitySerializer from .serializers import LocationSerializer from .serializers import StockTrackingSerializer @@ -237,16 +238,20 @@ class StockList(generics.ListCreateAPIView): - GET: Return a list of all StockItem objects (with optional query filters) - POST: Create a new StockItem + + Additional query parameters are available: + - location: Filter stock by location + - category: Filter by parts belonging to a certain category + - supplier: Filter by supplier """ def get_queryset(self): """ If the query includes a particular location, we may wish to also request stock items from all child locations. - This is set by the optional param 'include_child_categories' """ - # Does the client wish to filter by category? + # Does the client wish to filter by stock location? loc_id = self.request.query_params.get('location', None) # Start with all objects @@ -255,23 +260,28 @@ class StockList(generics.ListCreateAPIView): if loc_id: try: location = StockLocation.objects.get(pk=loc_id) - - # Filter by the supplied category - flt = Q(location=loc_id) - - if self.request.query_params.get('include_child_locations', None): - childs = location.getUniqueChildren() - for child in childs: - # Ignore the top-level category (already filtered!) - if str(child) == str(loc_id): - continue - flt |= Q(location=child) - - stock_list = stock_list.filter(flt) - + stock_list = stock_list.filter(location__in=location.getUniqueChildren()) + except StockLocation.DoesNotExist: pass + # Does the client wish to filter by part category? + cat_id = self.request.query_params.get('category', None) + + if cat_id: + try: + category = PartCategory.objects.get(pk=cat_id) + stock_list = stock_list.filter(part__category__in=category.getUniqueChildren()) + + except PartCategory.DoesNotExist: + pass + + # Filter by supplier + supplier_id = self.request.query_params.get('supplier', None) + + if supplier_id: + stock_list = stock_list.filter(supplier_part__supplier=supplier_id) + return stock_list serializer_class = StockItemSerializer diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index f05b14c77b..ae36078ece 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -44,24 +44,7 @@
-
-
- - -
-
- - -
- +{% include "stock_table.html" %} {% include 'modals.html' %} @@ -80,7 +63,15 @@ location: {{ location.id }} {% endif %} }, - follow: true + follow: true, + secondary: [ + { + field: 'parent', + label: 'New Location', + title: 'Create new location', + url: "{% url 'stock-location-create' %}", + }, + ] }); return false; }); @@ -141,46 +132,7 @@ return false; }); - - function selectedStock() { - return $("#stock-table").bootstrapTable('getSelections'); - } - - $("#multi-item-move").click(function() { - - var items = selectedStock(); - - moveStockItems(items, - { - success: function() { - $("#stock-table").bootstrapTable('refresh'); - } - }); - - return false; - }); - - $('#multi-item-stocktake').click(function() { - updateStockItems({ - action: 'stocktake', - }); - return false; - }); - - $('#multi-item-remove').click(function() { - updateStockItems({ - action: 'remove', - }); - return false; - }); - - $('#multi-item-add').click(function() { - updateStockItems({ - action: 'add', - }); - return false; - }); - + loadStockTable($("#stock-table"), { buttons: [ '#stock-options', @@ -188,7 +140,6 @@ params: { {% if location %} location: {{ location.id }}, - include_child_locations: true, {% endif %} }, url: "{% url 'api-stock-list' %}", diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index 187cc1addb..77730f5ea5 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% load static %} + {% block page_title %} InvenTree | Search Results {% endblock %} @@ -19,6 +21,11 @@ InvenTree | Search Results {% endblock %} +{% block js_load %} +{{ block.super }} + +{% endblock %} + {% block js_ready %} {{ block.super }} diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 5cb71d13ac..e7785abcd5 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -9,7 +9,7 @@
  • Parts
  • Stock
  • Build
  • -
  • Companies
  • +
  • Suppliers