diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 338e664252..71f87ffd19 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -445,9 +445,9 @@ class IndexView(TemplateView): # TODO - Is there a less expensive way to get these from the database context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()] - # Generate a list of buildable parts which have stock below their minimum values + # Generate a list of assembly parts which have stock below their minimum values # TODO - Is there a less expensive way to get these from the database - context['to_build'] = [part for part in Part.objects.filter(buildable=True) if part.need_to_restock()] + context['to_build'] = [part for part in Part.objects.filter(assembly=True) if part.need_to_restock()] return context diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index ad482b8dc6..f9ffc99953 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -51,7 +51,7 @@ class Build(models.Model): related_name='builds', limit_choices_to={ 'is_template': False, - 'buildable': True, + 'assembly': True, 'active': True }, help_text='Select part to build', diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 500b4a2bdf..d0beee54ce 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -51,7 +51,7 @@ class CategoryList(generics.ListCreateAPIView): filter_backends = [ DjangoFilterBackend, - # filters.SearchFilter, + filters.SearchFilter, filters.OrderingFilter, ] @@ -129,8 +129,8 @@ class PartList(generics.ListCreateAPIView): filter_fields = [ 'is_template', 'variant_of', - 'buildable', - 'consumable', + 'assembly', + 'component', 'trackable', 'purchaseable', 'salable', diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index 632a265e23..7d669cf49d 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -57,5 +57,5 @@ fields: name: 'Bob' description: 'Can we build it?' - buildable: true + assembly: true \ No newline at end of file diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index a2ec613429..2eb9600065 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -92,17 +92,17 @@ class EditPartForm(HelperForm): 'category', 'name', 'IPN', - 'is_template', - 'variant_of', 'description', 'keywords', + 'variant_of', + 'is_template', 'URL', 'default_location', 'default_supplier', 'units', 'minimum_stock', - 'buildable', - 'consumable', + 'assembly', + 'component', 'trackable', 'purchaseable', 'salable', diff --git a/InvenTree/part/migrations/0007_auto_20190602_1944.py b/InvenTree/part/migrations/0007_auto_20190602_1944.py new file mode 100644 index 0000000000..9bfeb7fb8d --- /dev/null +++ b/InvenTree/part/migrations/0007_auto_20190602_1944.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2 on 2019-06-02 09:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0006_auto_20190526_1215'), + ] + + operations = [ + migrations.RemoveField( + model_name='part', + name='buildable', + ), + migrations.RemoveField( + model_name='part', + name='consumable', + ), + migrations.AddField( + model_name='part', + name='assembly', + field=models.BooleanField(default=False, help_text='Can this part be built from other parts?', verbose_name='Assembly'), + ), + migrations.AddField( + model_name='part', + name='component', + field=models.BooleanField(default=True, help_text='Can this part be used to build other parts?', verbose_name='Component'), + ), + migrations.AlterField( + model_name='bomitem', + name='part', + field=models.ForeignKey(help_text='Select parent part', limit_choices_to={'active': True, 'assembly': True}, on_delete=django.db.models.deletion.CASCADE, related_name='bom_items', to='part.Part'), + ), + 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, 'component': True}, on_delete=django.db.models.deletion.CASCADE, related_name='used_in', to='part.Part'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 048330c2c6..f311aea1f1 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -201,8 +201,8 @@ class Part(models.Model): minimum_stock: Minimum preferred quantity to keep in stock units: Units of measure for this part (default='pcs') salable: Can this part be sold to customers? - buildable: Can this part be build from other parts? - consumable: Can this part be used to make other parts? + assembly: Can this part be build from other parts? + component: Can this part be used to make other parts? purchaseable: Can this part be purchased from suppliers? trackable: Trackable parts can have unique serial numbers assigned, etc, etc active: Is this part active? Parts are deactivated instead of being deleted @@ -248,6 +248,18 @@ class Part(models.Model): else: return static('/img/blank_image.png') + def validate_unique(self, exclude=None): + super().validate_unique(exclude) + + # Part name uniqueness should be case insensitive + try: + if Part.objects.filter(name__iexact=self.name).exclude(id=self.id).exists(): + raise ValidationError({ + "name": _("A part with this name already exists") + }) + except Part.DoesNotExist: + pass + def clean(self): """ Perform cleaning operations for the Part model """ @@ -343,9 +355,9 @@ class Part(models.Model): units = models.CharField(max_length=20, default="pcs", blank=True, help_text='Stock keeping units for this part') - buildable = models.BooleanField(default=False, help_text='Can this part be built from other parts?') + assembly = models.BooleanField(default=False, verbose_name='Assembly', help_text='Can this part be built from other parts?') - consumable = models.BooleanField(default=True, help_text='Can this part be used to build other parts?') + component = models.BooleanField(default=True, verbose_name='Component', help_text='Can this part be used to build other parts?') trackable = models.BooleanField(default=False, help_text='Does this part have tracking for unique items?') @@ -858,7 +870,7 @@ class BomItem(models.Model): part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items', help_text='Select parent part', limit_choices_to={ - 'buildable': True, + 'assembly': True, 'active': True, }) @@ -867,7 +879,7 @@ class BomItem(models.Model): sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in', help_text='Select part to be used in BOM', limit_choices_to={ - 'consumable': True, + 'component': True, 'active': True }) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index fe858091c7..224e41cd97 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -94,8 +94,8 @@ class PartSerializer(serializers.ModelSerializer): # 'available_stock', 'units', 'trackable', - 'buildable', - 'consumable', + 'assembly', + 'component', 'trackable', 'salable', 'active', diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index fd8f20d2aa..2d76b08ac9 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -156,6 +156,7 @@ {% endif %} }, buttons: ['#part-options'], + checkbox: true, }, ); diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 9765a2574e..b536a5542e 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -90,30 +90,6 @@ Units {{ part.units }} - - -
- - - - - - - - - - - - - - - - - - - - - {% if part.minimum_stock > 0 %} @@ -122,6 +98,40 @@ {% endif %}
Buildable{% include "yesnolabel.html" with value=part.buildable %}
Consumable{% include "yesnolabel.html" with value=part.consumable %}
Trackable{% include "yesnolabel.html" with value=part.trackable %}
Purchaseable{% include "yesnolabel.html" with value=part.purchaseable %}
Salable{% include "yesnolabel.html" with value=part.salable %}
Minimum Stock
+
+ + {% if part.assembly %} + + + + + {% endif %} + {% if part.component %} + + + + + {% endif %} + {% if part.trackable %} + + + + + {% endif %} + {% if part.purchaseable %} + + + + + {% endif %} + {% if part.salable %} + + + + + {% endif %} +
AssemblyThis part can be assembled from other parts
ComponentThis part can be used in assemblies
TrackableStock for this part will be tracked by (serial or batch)
PurchaseableThis part can be purchased from external suppliers
SalableThis part can be sold to customers
+
{% if part.notes %} diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index d8af6d2ec9..1354a9a4a4 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -78,7 +78,7 @@ In Stock {{ part.total_stock }} - {% if part.buildable %} + {% if part.assembly %} Can Build {{ part.can_build }} diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 53a551f36c..dd5034040d 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -15,13 +15,13 @@ Allocated {{ part.allocation_count }} {% endif %} - {% if part.buildable %} + {% if part.assembly %} BOM{{ part.bom_count }} Build{{ part.active_builds|length }} {% endif %} - {% if part.consumable or part.used_in_count > 0 %} + {% if part.component or part.used_in_count > 0 %} Used In{% if part.used_in_count > 0 %}{{ part.used_in_count }}{% endif %} {% endif %} diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 15c6f58dcd..b0859e5c1e 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -149,7 +149,7 @@ class PartAPITest(APITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - # Now try to create a BomItem which points to a non-buildable part (should fail) + # Now try to create a BomItem which points to a non-assembly part (should fail) data['part'] = 3 response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/InvenTree/static/script/inventree/inventree.js b/InvenTree/static/script/inventree/inventree.js index 48ba874262..703c49218b 100644 --- a/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/static/script/inventree/inventree.js @@ -124,6 +124,10 @@ function imageHoverIcon(url) { * On mouseover, display a full-size version of the image */ + if (!url) { + url = '/static/img/blank_image.png'; + } + var html = ` diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index 1001ef11a1..7c933ef303 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -82,6 +82,7 @@ function loadPartTable(table, url, options={}) { * - url: Base URL for API query * - options: object containing following (optional) fields * allowInactive: If true, allow display of inactive parts + * checkbox: Show the checkbox column * query: extra query params for API request * buttons: If provided, link buttons to selection status of this table */ @@ -94,6 +95,84 @@ function loadPartTable(table, url, options={}) { query.active = true; } + var columns = [ + { + field: 'pk', + title: 'ID', + visible: false, + } + ]; + + if (options.checkbox) { + columns.push({ + checkbox: true, + title: 'Select', + searchable: false, + }); + } + + columns.push({ + field: 'full_name', + title: 'Part', + sortable: true, + formatter: function(value, row, index, field) { + + if (row.is_template) { + value = '' + value + ''; + } + + var display = imageHoverIcon(row.image_url) + renderLink(value, row.url); + + if (!row.active) { + display = display + "INACTIVE"; + } + return display; + } + }); + + columns.push({ + sortable: true, + field: 'description', + title: 'Description', + formatter: function(value, row, index, field) { + + if (row.is_template) { + value = '' + value + ''; + } + + return value; + } + }); + + columns.push({ + sortable: true, + field: 'category_name', + title: 'Category', + formatter: function(value, row, index, field) { + if (row.category) { + return renderLink(row.category_name, "/part/category/" + row.category + "/"); + } + else { + return ''; + } + } + }); + + columns.push({ + field: 'total_stock', + title: 'Stock', + searchable: false, + sortable: true, + formatter: function(value, row, index, field) { + if (value) { + return renderLink(value, row.url + 'stock/'); + } + else { + return "No Stock"; + } + } + }); + $(table).bootstrapTable({ url: url, sortable: true, @@ -107,76 +186,7 @@ function loadPartTable(table, url, options={}) { queryParams: function(p) { return query; }, - columns: [ - { - checkbox: true, - title: 'Select', - searchable: false, - }, - { - field: 'pk', - title: 'ID', - visible: false, - }, - { - field: 'full_name', - title: 'Part', - sortable: true, - formatter: function(value, row, index, field) { - - if (row.is_template) { - value = '' + value + ''; - } - - var display = imageHoverIcon(row.image_url) + renderLink(value, row.url); - - if (!row.active) { - display = display + "INACTIVE"; - } - return display; - } - }, - { - sortable: true, - field: 'description', - title: 'Description', - formatter: function(value, row, index, field) { - - if (row.is_template) { - value = '' + value + ''; - } - - return value; - } - }, - { - sortable: true, - field: 'category_name', - title: 'Category', - formatter: function(value, row, index, field) { - if (row.category) { - return renderLink(row.category_name, "/part/category/" + row.category + "/"); - } - else { - return ''; - } - } - }, - { - field: 'total_stock', - title: 'Stock', - searchable: false, - sortable: true, - formatter: function(value, row, index, field) { - if (value) { - return renderLink(value, row.url + 'stock/'); - } - else { - return "No Stock"; - } - } - } - ], + columns: columns, }); if (options.buttons) { diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 65b0c55da5..facbaa65f5 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -153,7 +153,10 @@ function loadStockTable(table, options) { var text = renderLink(val, '/stock/item/' + row.pk + '/'); - text = text + "" + row.status_text + ""; + if (row.status_text != 'OK') { + text = text + "" + row.status_text + ""; + } + return text; } }, diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index c1c2b8e1cf..b1c746cdd6 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -236,6 +236,11 @@ class StockLocationList(generics.ListCreateAPIView): 'parent', ] + search_fields = [ + 'name', + 'description', + ] + class StockList(generics.ListCreateAPIView): """ API endpoint for list view of Stock objects @@ -306,6 +311,8 @@ class StockList(generics.ListCreateAPIView): else: item['location__path'] = None + item['status_text'] = StockItem.ITEM_STATUS_CODES[item['status']] + return Response(data) def get_queryset(self): diff --git a/InvenTree/stock/migrations/0005_auto_20190602_1944.py b/InvenTree/stock/migrations/0005_auto_20190602_1944.py new file mode 100644 index 0000000000..1d134288cc --- /dev/null +++ b/InvenTree/stock/migrations/0005_auto_20190602_1944.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-06-02 09:44 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0004_auto_20190525_2356'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost')], default=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4efa347ecd..4434741186 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -260,12 +260,14 @@ class StockItem(models.Model): ITEM_ATTENTION = 50 ITEM_DAMAGED = 55 ITEM_DESTROYED = 60 + ITEM_LOST = 70 ITEM_STATUS_CODES = { ITEM_OK: _("OK"), ITEM_ATTENTION: _("Attention needed"), ITEM_DAMAGED: _("Damaged"), - ITEM_DESTROYED: _("Destroyed") + ITEM_DESTROYED: _("Destroyed"), + ITEM_LOST: _("Lost") } status = models.PositiveIntegerField( diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index f8631dfbd4..70915a49fd 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -17,10 +17,18 @@ InvenTree | Search Results


+
+

No results found

+
+ +{% include "InvenTree/search_part_category.html" with collapse_id="categories" %} + {% include "InvenTree/search_parts.html" with collapse_id='parts' %} {% include "InvenTree/search_supplier_parts.html" with collapse_id='supplier_parts' %} +{% include "InvenTree/search_stock_location.html" with collapse_id="locations" %} + {% endblock %} {% block js_load %} @@ -31,29 +39,86 @@ InvenTree | Search Results {% block js_ready %} {{ block.super }} + $(".panel-group").hide(); + function onSearchResults(table, output) { $(table).on('load-success.bs.table', function() { + + var panel = $(output).closest('.panel-group'); + var n = $(table).bootstrapTable('getData').length; var text = ''; if (n == 0) { text = 'No results' + + $(panel).hide(); + + } else { text = n + ' result'; if (n > 1) { text += 's'; } + + $(panel).show(); + + $("#no-search-results").hide(); } $(output).html(text); }); } + onSearchResults("#category-results-table", "#category-results-count"); + + onSearchResults("#location-results-table", "#location-results-count"); + onSearchResults('#part-results-table', '#part-result-count'); onSearchResults('#supplier-part-results-table', '#supplier-part-result-count'); + $("#category-results-table").bootstrapTable({ + url: "{% url 'api-part-category-list' %}", + queryParams: { + search: "{{ query }}", + }, + columns: [ + { + field: 'name', + title: 'Name', + formatter: function(value, row, index, field) { + return renderLink(value, '/part/category/' + row.pk + '/'); + }, + }, + { + field: 'description', + title: 'Description', + }, + ], + }); + + $("#location-results-table").bootstrapTable({ + url: "{% url 'api-location-list' %}", + queryParams: { + search: "{{ query }}", + }, + columns: [ + { + field: 'name', + title: 'Name', + formatter: function(value, row, index, field) { + return renderLink(value, '/stock/location/' + row.pk + '/'); + }, + }, + { + field: 'description', + title: 'Description', + }, + ], + }); + loadPartTable("#part-results-table", "{% url 'api-part-list' %}", { @@ -61,6 +126,7 @@ InvenTree | Search Results search: "{{ query }}", }, allowInactive: true, + checkbox: false, } ); diff --git a/InvenTree/templates/InvenTree/search_part_category.html b/InvenTree/templates/InvenTree/search_part_category.html new file mode 100644 index 0000000000..899aca094c --- /dev/null +++ b/InvenTree/templates/InvenTree/search_part_category.html @@ -0,0 +1,14 @@ +{% extends "collapse.html" %} + +{% block collapse_title %} +

Part Categories

+{% endblock %} + +{% block collapse_heading %} +

{% include "InvenTree/searching.html" %}

+{% endblock %} + +{% block collapse_content %} + +
+{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/search_stock_location.html b/InvenTree/templates/InvenTree/search_stock_location.html new file mode 100644 index 0000000000..923635b13e --- /dev/null +++ b/InvenTree/templates/InvenTree/search_stock_location.html @@ -0,0 +1,14 @@ +{% extends "collapse.html" %} + +{% block collapse_title %} +

Stock Locations

+{% endblock %} + +{% block collapse_heading %} +

{% include "InvenTree/searching.html" %}

+{% endblock %} + +{% block collapse_content %} + +
+{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/searching.html b/InvenTree/templates/InvenTree/searching.html index 9d111fe257..5821515ad7 100644 --- a/InvenTree/templates/InvenTree/searching.html +++ b/InvenTree/templates/InvenTree/searching.html @@ -1 +1 @@ - Searching... \ No newline at end of file + Searching \ No newline at end of file diff --git a/InvenTree/templates/search_form.html b/InvenTree/templates/search_form.html index 72d317d3ec..e9c2214731 100644 --- a/InvenTree/templates/search_form.html +++ b/InvenTree/templates/search_form.html @@ -3,5 +3,5 @@
- +