diff --git a/InvenTree/build/migrations/0011_auto_20190508_0748.py b/InvenTree/build/migrations/0011_auto_20190508_0748.py new file mode 100644 index 0000000000..e4c0ec5fa7 --- /dev/null +++ b/InvenTree/build/migrations/0011_auto_20190508_0748.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-07 21:48 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0010_auto_20190505_2233'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index e81ae6a45d..d4a8e70448 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -13,9 +13,11 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction +from django.db.models import Sum from django.core.validators import MinValueValidator from stock.models import StockItem +from part.models import BomItem class Build(models.Model): @@ -33,49 +35,6 @@ class Build(models.Model): notes: Text notes """ - def save(self, *args, **kwargs): - """ Called when the Build model is saved to the database. - - If this is a new Build, try to allocate StockItem objects automatically. - - - If there is only one StockItem for a Part, use that one. - - If there are multiple StockItem objects, leave blank and let the user decide - """ - - allocate_parts = False - - # If there is no PK yet, then this is the first time the Build has been saved - if not self.pk: - allocate_parts = True - - # Save this Build first - super(Build, self).save(*args, **kwargs) - - if allocate_parts: - for item in self.part.bom_items.all(): - part = item.sub_part - # Number of parts required for this build - q_req = item.quantity * self.quantity - - stock = StockItem.objects.filter(part=part) - - if len(stock) == 1: - stock_item = stock[0] - - # Are there any parts available? - if stock_item.quantity > 0: - # If there are not enough parts, reduce the amount we will take - if stock_item.quantity < q_req: - q_req = stock_item.quantity - - # Allocate parts to this build - build_item = BuildItem( - build=self, - stock_item=stock_item, - quantity=q_req) - - build_item.save() - def __str__(self): return "Build {q} x {part}".format(q=self.quantity, part=str(self.part)) @@ -103,11 +62,13 @@ class Build(models.Model): # Build status codes PENDING = 10 # Build is pending / active + ALLOCATED = 20 # Parts have been removed from stock CANCELLED = 30 # Build was cancelled COMPLETE = 40 # Build is complete #: Build status codes BUILD_STATUS_CODES = {PENDING: _("Pending"), + ALLOCATED: _("Allocated"), CANCELLED: _("Cancelled"), COMPLETE: _("Complete"), } @@ -153,6 +114,68 @@ class Build(models.Model): self.status = self.CANCELLED self.save() + def getAutoAllocations(self): + """ Return a list of parts which will be allocated + using the 'AutoAllocate' function. + + For each item in the BOM for the attached Part: + + - If there is a single StockItem, use that StockItem + - Take as many parts as available (up to the quantity required for the BOM) + - If there are multiple StockItems available, ignore (leave up to the user) + + Returns: + A dict object containing the StockItem objects to be allocated (and the quantities) + """ + + allocations = {} + + for item in self.part.bom_items.all(): + + # How many parts required for this build? + q_required = item.quantity * self.quantity + + stock = StockItem.objects.filter(part=item.sub_part) + + # Only one StockItem to choose from? Default to that one! + if len(stock) == 1: + stock_item = stock[0] + + # Check that we have not already allocated this stock-item against this build + build_items = BuildItem.objects.filter(build=self, stock_item=stock_item) + + if len(build_items) > 0: + continue + + # Are there any parts available? + if stock_item.quantity > 0: + # Only take as many as are available + if stock_item.quantity < q_required: + q_required = stock_item.quantity + + # Add the item to the allocations list + allocations[stock_item] = q_required + + return allocations + + @transaction.atomic + def autoAllocate(self): + """ Run auto-allocation routine to allocate StockItems to this Build. + + See: getAutoAllocations() + """ + + allocations = self.getAutoAllocations() + + for item in allocations: + # Create a new allocation + build_item = BuildItem( + build=self, + stock_item=item, + quantity=allocations[item]) + + build_item.save() + @transaction.atomic def completeBuild(self, location, user): """ Mark the Build as COMPLETE @@ -200,6 +223,41 @@ class Build(models.Model): self.status = self.COMPLETE self.save() + def getRequiredQuantity(self, part): + """ Calculate the quantity of required to make this build. + """ + + try: + item = BomItem.objects.get(part=self.part.id, sub_part=part.id) + return item.quantity * self.quantity + except BomItem.DoesNotExist: + return 0 + + def getAllocatedQuantity(self, part): + """ Calculate the total number of currently allocated to this build + """ + + allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(Sum('quantity')) + + q = allocated['quantity__sum'] + + if q: + return int(q) + else: + return 0 + + def getUnallocatedQuantity(self, part): + """ Calculate the quantity of which still needs to be allocated to this build. + + Args: + Part - the part to be tested + + Returns: + The remaining allocated quantity + """ + + return max(self.getRequiredQuantity(part) - self.getAllocatedQuantity(part), 0) + @property def required_parts(self): """ Returns a dict of parts required to build this part (BOM) """ @@ -208,7 +266,8 @@ class Build(models.Model): for item in self.part.bom_items.all(): part = {'part': item.sub_part, 'per_build': item.quantity, - 'quantity': item.quantity * self.quantity + 'quantity': item.quantity * self.quantity, + 'allocated': self.getAllocatedQuantity(item.sub_part) } parts.append(part) diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 0a3867af8e..d6657ea196 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -6,17 +6,24 @@

Allocate Parts for Build

-

{{ build.title }}

-{{ build.quantity }} x {{ build.part.name }} +
+
+ +

{{ build.title }}

+ {{ build.quantity }} x {{ build.part.name }} +
+
+
+ +
+
+

{% for bom_item in bom_items.all %} {% include "build/allocation_item.html" with item=bom_item build=build collapse_id=bom_item.id %} {% endfor %} -
- -
{% endblock %} diff --git a/InvenTree/build/templates/build/allocation_item.html b/InvenTree/build/templates/build/allocation_item.html index f9e92a63a8..3468d6d794 100644 --- a/InvenTree/build/templates/build/allocation_item.html +++ b/InvenTree/build/templates/build/allocation_item.html @@ -1,9 +1,23 @@ {% extends "collapse.html" %} +{% load static %} {% load inventree_extras %} {% block collapse_title %} -{{ item.sub_part.name }} +
+
+ {{ item.sub_part.name }} + {% else %} + src="{% static 'img/blank_image.png' %}" alt='No image'> + {% endif %} +
+
+ {{ item.sub_part.name }}
+ {{ item.sub_part.description }} +
+
{% endblock %} {% block collapse_heading %} diff --git a/InvenTree/build/templates/build/build_list.html b/InvenTree/build/templates/build/build_list.html index 890dd551b1..6c28f47eda 100644 --- a/InvenTree/build/templates/build/build_list.html +++ b/InvenTree/build/templates/build/build_list.html @@ -12,6 +12,11 @@ Part Quantity Status + {% if completed %} + Completed + {% else %} + Created + {% endif %} @@ -21,6 +26,11 @@ {{ build.part.name }} {{ build.quantity }} {% include "build_status.html" with build=build %} + {% if completed %} + {{ build.completion_date }}{{ build.completed_by.username }} + {% else %} + {{ build.creation_date }} + {% endif %} {% endfor %} diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 96fad2caa5..9ca444f516 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -85,7 +85,8 @@ Part Required - Stock + Available + Allocated @@ -94,6 +95,7 @@ {{ item.part.name }} {{ item.quantity }} {{ item.part.total_stock }} + {{ item.allocated }} {% endfor %} diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index 46d3533651..af24a6ca53 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -3,39 +3,26 @@ {% block content %} -

Active Builds

- -
-
- +
+
+

Part Builds

+
+
+
+
+ +
+
- - - - - - - - - - -{% for build in active %} - - - - - -{% endfor %} - -
BuildPartQuantityStatus
{{ build.title }}{{ build.part.name }}{{ build.quantity }}{% include "build_status.html" with build=build %} -
+
-

-{% include "build/build_list.html" with builds=completed title="Completed Builds" collapse_id="complete" %} -

-{% include "build/build_list.html" with builds=cancelled title="Cancelled Builds" collapse_id="cancelled" %} +{% include "build/build_list.html" with builds=active title="Active Builds" completed=False collapse_id='active' %} + +{% include "build/build_list.html" with builds=completed completed=True title="Completed Builds" collapse_id="complete" %} + +{% include "build/build_list.html" with builds=cancelled title="Cancelled Builds" completed=False collapse_id="cancelled" %} {% include 'modals.html' %} @@ -43,6 +30,9 @@ {% block js_ready %} {{ block.super }} + + $("#collapse-item-active").collapse().show(); + $("#new-build").click(function() { launchModalForm( "{% url 'build-create' %}", @@ -74,7 +64,10 @@ { title: 'Status', sortable: true, - } + }, + { + sortable: true, + }, ] }); diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index cd2c87e814..19af4b6458 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -289,6 +289,16 @@ class BuildItemCreate(AjaxCreateView): query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)]) form.fields['stock_item'].queryset = query + + stocks = query.all() + # If there is only one item selected, select it + if len(stocks) == 1: + form.fields['stock_item'].initial = stocks[0].id + # There is no stock available + elif len(stocks) == 0: + # TODO - Add a message to the form describing the problem + pass + except Part.DoesNotExist: pass @@ -303,10 +313,24 @@ class BuildItemCreate(AjaxCreateView): initials = super(AjaxCreateView, self).get_initial().copy() build_id = self.get_param('build') + part_id = self.get_param('part') + + if part_id: + try: + part = Part.objects.get(pk=part_id) + except Part.DoesNotExist: + part = None if build_id: try: - initials['build'] = Build.objects.get(pk=build_id) + build = Build.objects.get(pk=build_id) + initials['build'] = build + + # Try to work out how many parts to allocate + if part: + unallocated = build.getUnallocatedQuantity(part) + initials['quantity'] = unallocated + except Build.DoesNotExist: pass diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 1d119ddf51..bb86230b70 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -9,6 +9,7 @@ import os from django.db import models from django.urls import reverse +from django.conf import settings def rename_company_image(instance, filename): @@ -78,6 +79,14 @@ class Company(models.Model): """ Get the web URL for the detail view for this Company """ return reverse('company-detail', kwargs={'pk': self.id}) + def get_image_url(self): + """ Return the URL of the image for this company """ + + if self.image: + return os.path.join(settings.MEDIA_URL, str(self.image.url)) + else: + return '' + @property def part_count(self): """ The number of parts supplied by this company """ diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index 797446472f..24982c021f 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -51,10 +51,10 @@ }, { sortable: true, - field: 'part_name', + field: 'part_detail.name', title: 'Part', formatter: function(value, row, index, field) { - return renderLink(value, '/part/' + row.part + '/suppliers/'); + return imageHoverIcon(row.part_detail.image_url) + renderLink(value, '/part/' + row.part + '/suppliers/'); } }, { diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 7cfd7d18de..4fa19e9eb6 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -4,11 +4,21 @@ {% block content %} -

Companies

-
- +
+
+

Company List

+
+
+
+
+ +
+
+
+
+
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 2a3c31552e..3bca8cd445 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -14,6 +14,7 @@ import tablib from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from django.urls import reverse +from django.conf import settings from django.db import models from django.core.validators import MinValueValidator @@ -120,8 +121,17 @@ class Part(models.Model): """ def get_absolute_url(self): + """ Return the web URL for viewing this part """ return reverse('part-detail', kwargs={'pk': self.id}) + def get_image_url(self): + """ Return the URL of the image for this part """ + + if self.image: + return os.path.join(settings.MEDIA_URL, str(self.image.url)) + else: + return '' + # Short name of the part name = models.CharField(max_length=100, unique=True, blank=False, help_text='Part name (must be unique)') diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 847b3bb1a8..97588650cb 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -33,6 +33,7 @@ class PartBriefSerializer(serializers.ModelSerializer): """ Serializer for Part (brief detail) """ url = serializers.CharField(source='get_absolute_url', read_only=True) + image_url = serializers.CharField(source='get_image_url', read_only=True) class Meta: model = Part @@ -40,6 +41,7 @@ class PartBriefSerializer(serializers.ModelSerializer): 'pk', 'url', 'name', + 'image_url', 'description', 'available_stock', ] @@ -51,6 +53,7 @@ class PartSerializer(serializers.ModelSerializer): """ url = serializers.CharField(source='get_absolute_url', read_only=True) + image_url = serializers.CharField(source='get_image_url', read_only=True) category_name = serializers.CharField(source='category_path', read_only=True) class Meta: @@ -60,6 +63,7 @@ class PartSerializer(serializers.ModelSerializer): 'pk', 'url', # Link to the part detail page 'name', + 'image_url', 'IPN', 'URL', # Link to an external URL (optional) 'description', @@ -121,9 +125,10 @@ class SupplierPartSerializer(serializers.ModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) - part_name = serializers.CharField(source='part.name', read_only=True) + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) supplier_name = serializers.CharField(source='supplier.name', read_only=True) + supplier_logo = serializers.CharField(source='supplier.get_image_url', read_only=True) class Meta: model = SupplierPart @@ -131,9 +136,10 @@ class SupplierPartSerializer(serializers.ModelSerializer): 'pk', 'url', 'part', - 'part_name', + 'part_detail', 'supplier', 'supplier_name', + 'supplier_logo', 'SKU', 'manufacturer', 'MPN', diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index d1aa037870..77ffa8dbff 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -5,12 +5,19 @@ {% include 'part/tabs.html' with tab='attachments' %} -

Attachments

- -
- +
+
+

Part Attachments

+
+
+
+ +
+
+
+ diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 7dad872114..b9695ea6f2 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -119,81 +119,18 @@ {% endif %} - $("#part-table").bootstrapTable({ - sortable: true, - search: true, - sortName: 'description', - idField: 'pk', - method: 'get', - pagination: true, - rememberOrder: true, - queryParams: function(p) { - return { - active: true, + loadPartTable( + "#part-table", + "{% url 'api-part-list' %}", + { + query: { {% if category %} category: {{ category.id }}, include_child_categories: true, {% endif %} - } + }, + buttons: ['#part-options'], }, - columns: [ - { - checkbox: true, - title: 'Select', - searchable: false, - }, - { - field: 'pk', - title: 'ID', - visible: false, - }, - { - field: 'name', - title: 'Part', - sortable: true, - formatter: function(value, row, index, field) { - return renderLink(value, row.url); - } - }, - { - sortable: true, - field: 'description', - title: 'Description', - }, - { - 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"; - } - } - } - ], - url: "{% url 'api-part-list' %}", - }); - - linkButtonsToSelection( - $("#part-table"), - ['#part-options'] ); {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index a59b97ce47..419409a95d 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -6,26 +6,24 @@
-

Part Details

+

Part Details

-

- -

+
+ + +
diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html index fb620c1b7b..a329a25249 100644 --- a/InvenTree/part/templates/part/stock.html +++ b/InvenTree/part/templates/part/stock.html @@ -4,7 +4,14 @@ {% include 'part/tabs.html' with tab='stock' %} -

Part Stock

+
+
+

Part Stock

+
+
+
+
+
{% if part.active %} diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index 0f417eb650..4999694651 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -4,7 +4,15 @@ {% include 'part/tabs.html' with tab='suppliers' %} -

Part Suppliers

+
+
+

Part Suppliers

+
+
+
+
+ +
@@ -55,7 +63,7 @@ field: 'supplier_name', title: 'Supplier', formatter: function(value, row, index, field) { - return renderLink(value, '/company/' + row.supplier + '/'); + return imageHoverIcon(row.supplier_logo) + renderLink(value, '/company/' + row.supplier + '/'); } }, { diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 2bcf894b2a..d8e84a6f09 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -37,4 +37,6 @@ Attachments {% if part.attachments.all|length > 0 %}{{ part.attachments.all|length }}{% endif %} - \ No newline at end of file + + +
\ No newline at end of file diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index 118f335468..f1a055695e 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -4,7 +4,17 @@ {% include 'part/tabs.html' with tab='used' %} -

Used In

+
+
+

Used to Build

+
+
+
+
+
+
+ +
File
@@ -33,7 +43,7 @@ field: 'part_detail', title: 'Part', formatter: function(value, row, index, field) { - return renderLink(value.name, value.url + 'bom/'); + return imageHoverIcon(row.part_detail.image_url) + renderLink(value.name, value.url + 'bom/'); } }, { diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index c97808888e..a018d578bf 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -404,12 +404,30 @@ class CategoryEdit(AjaxUpdateView): context = super(CategoryEdit, self).get_context_data(**kwargs).copy() try: - context['category'] = PartCategory.objects.get(pk=self.kwargs['pk']) + context['category'] = self.get_object() except: pass return context + def get_form(self): + """ Customize form data for PartCategory editing. + + Limit the choices for 'parent' field to those which make sense + """ + + form = super(AjaxUpdateView, self).get_form() + + category = self.get_object() + + # Remove any invalid choices for the parent category part + parent_choices = PartCategory.objects.all() + parent_choices = parent_choices.exclude(id__in=category.getUniqueChildren()) + + form.fields['parent'].queryset = parent_choices + + return form + class CategoryDelete(AjaxDeleteView): """ Delete view to delete a PartCategory """ diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 78483daac5..5147547275 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -10,6 +10,32 @@ color: #ffcc00; } +/* Part image icons with full-display on mouse hover */ + +.hover-img-thumb { + background: #eee; + width: 28px; + height: 28px; + border: 1px solid #cce; +} + +.hover-img-large { + background: #eee; + display: none; + position: absolute; + z-index: 999; + border: 1px solid #555; + max-width: 250px; +} + +.hover-icon { + margin-right: 10px; +} + +.hover-icon:hover > .hover-img-large { + display: block; +} + /* dropzone class - for Drag-n-Drop file uploads */ .dropzone { border: 1px solid #555; @@ -159,6 +185,14 @@ margin-bottom: 5px; } +.panel-group .panel { + border-radius: 8px; +} + +.panel-heading { + padding: 5px 10px; +} + .float-right { float: right; } diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 777acbd4ef..b96ae49050 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -113,6 +113,16 @@ function loadBomTable(table, options) { sortable: true, } ); + + // Part notes + cols.push( + { + field: 'note', + title: 'Notes', + searchable: true, + sortable: true, + } + ); if (options.editable) { cols.push({ @@ -124,6 +134,7 @@ function loadBomTable(table, options) { } }); } + else { cols.push( { @@ -148,16 +159,6 @@ function loadBomTable(table, options) { } ); } - - // Part notes - cols.push( - { - field: 'note', - title: 'Notes', - searchable: true, - sortable: false, - } - ); // Configure the table (bootstrap-table) diff --git a/InvenTree/static/script/inventree/inventree.js b/InvenTree/static/script/inventree/inventree.js index cc025477c0..48ba874262 100644 --- a/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/static/script/inventree/inventree.js @@ -117,4 +117,19 @@ function enableDragAndDrop(element, url, options) { console.log('Ignoring drag-and-drop event (not a file)'); } }); +} + +function imageHoverIcon(url) { + /* Render a small thumbnail icon for an image. + * On mouseover, display a full-size version of the image + */ + + var html = ` + + + + + `; + + return html; } \ No newline at end of file diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index c3b0f8182d..a991cb232c 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -72,4 +72,100 @@ function toggleStar(options) { }, } ); +} + +function loadPartTable(table, url, options={}) { + /* Load part listing data into specified table. + * + * Args: + * - table: HTML reference to the table + * - url: Base URL for API query + * - options: object containing following (optional) fields + * allowInactive: If true, allow display of inactive parts + * query: extra query params for API request + * buttons: If provided, link buttons to selection status of this table + */ + + // Default query params + query = options.query; + + if (!options.allowInactive) { + // Only display active parts + query.active = true; + } + + $(table).bootstrapTable({ + url: url, + sortable: true, + search: true, + sortName: 'name', + method: 'get', + pagination: true, + pageSize: 25, + rememberOrder: true, + formatNoMatches: function() { return "No parts found"; }, + queryParams: function(p) { + return query; + }, + columns: [ + { + checkbox: true, + title: 'Select', + searchable: false, + }, + { + field: 'pk', + title: 'ID', + visible: false, + }, + { + field: 'name', + title: 'Part', + sortable: true, + formatter: function(value, row, index, field) { + var display = imageHoverIcon(row.image_url) + renderLink(value, row.url); + if (!row.active) { + display = display + "INACTIVE"; + } + return display; + } + }, + { + sortable: true, + field: 'description', + title: 'Description', + }, + { + 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"; + } + } + } + ], + }); + + if (options.buttons) { + linkButtonsToSelection($(table), options.buttons); + } } \ No newline at end of file diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 71bbed2ab3..2f55456cdb 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -347,6 +347,7 @@ function loadStockTable(table, options) { search: true, method: 'get', pagination: true, + pageSize: 25, rememberOrder: true, queryParams: options.params, columns: [ @@ -365,9 +366,14 @@ function loadStockTable(table, options) { title: 'Part', sortable: true, formatter: function(value, row, index, field) { - return renderLink(value, row.part.url); + return imageHoverIcon(row.part.image_url) + renderLink(value, row.part.url); } }, + { + field: 'part.description', + title: 'Description', + sortable: true, + }, { field: 'location', title: 'Location', diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 1f6f85ea1a..e618be1ee8 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -75,6 +75,24 @@ class StockLocationEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit Stock Location' + def get_form(self): + """ Customize form data for StockLocation editing. + + Limit the choices for 'parent' field to those which make sense. + """ + + form = super(AjaxUpdateView, self).get_form() + + location = self.get_object() + + # Remove any invalid choices for the 'parent' field + parent_choices = StockLocation.objects.all() + parent_choices = parent_choices.exclude(id__in=location.getUniqueChildren()) + + form.fields['parent'].queryset = parent_choices + + return form + class StockLocationQRCode(QRCodeView): """ View for displaying a QR code for a StockLocation object """ diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 12019ca7b0..89b412bdcc 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -2,7 +2,7 @@ {% block content %}

InvenTree

- +
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %} {% if to_order %} diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index e868378ee4..cd700facc8 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -22,57 +22,15 @@ var n = $("#part-results-table").bootstrapTable('getData').length; $("#part-result-count").html("(found " + n + " results)"); }); - - $("#part-results-table").bootstrapTable({ - sortable: true, - search: true, - pagination: true, - formatNoMatches: function() { return "No parts found matching search query"; }, - queryParams: function(p) { - return { + + loadPartTable("#part-results-table", + "{% url 'api-part-list' %}", + { + query: { search: "{{ query }}", - } - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, }, - { - field: 'name', - title: 'Name', - sortable: true, - searchable: true, - formatter: function(value, row, index, field) { - return renderLink(value, row.url); - } - }, - { - field: 'IPN', - title: 'Internal Part Number', - searchable: true, - }, - { - field: 'description', - title: 'Description', - searchable: true, - }, - { - field: 'available_stock', - title: 'Stock', - formatter: function(value, row, index, field) { - if (value) { - return renderLink(value, row.url + 'stock/'); - } else { - return renderLink('No stock', row.url + 'stock/'); - } - } - }, - ], - url: "{% url 'api-part-list' %}" - }); - - - + allowInactive: true, + } + ); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 84e2e10963..90925cfe7e 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -86,6 +86,7 @@ InvenTree + diff --git a/InvenTree/templates/build_status.html b/InvenTree/templates/build_status.html index 1702ceccae..3fc5f6086c 100644 --- a/InvenTree/templates/build_status.html +++ b/InvenTree/templates/build_status.html @@ -1,7 +1,7 @@ {% if build.status == build.PENDING %} -{% elif build.status == build.HOLDING %} - +{% elif build.status == build.ALLOCATED %} + {% elif build.status == build.CANCELLED %} {% elif build.status == build.COMPLETE %} diff --git a/InvenTree/templates/modal_form.html b/InvenTree/templates/modal_form.html index 566671c657..3c0674fec8 100644 --- a/InvenTree/templates/modal_form.html +++ b/InvenTree/templates/modal_form.html @@ -14,6 +14,9 @@ {% crispy form %} + + {% block form_data %} + {% endblock %} {% block post_form_content %}