diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py new file mode 100644 index 0000000000..ebdb8953fa --- /dev/null +++ b/InvenTree/InvenTree/helpers.py @@ -0,0 +1,32 @@ +import io + +from wsgiref.util import FileWrapper +from django.http import StreamingHttpResponse + + +def WrapWithQuotes(text): + # TODO - Make this better + if not text.startswith('"'): + text = '"' + text + + if not text.endswith('"'): + text = text + '"' + + return text + + +def DownloadFile(data, filename, content_type='application/text'): + """ + Create a dynamic file for the user to download. + @param data is the raw file data + """ + + filename = WrapWithQuotes(filename) + + wrapper = FileWrapper(io.StringIO(data)) + + response = StreamingHttpResponse(wrapper, content_type=content_type) + response['Content-Length'] = len(data) + response['Content-Disposition'] = 'attachment; filename={f}'.format(f=filename) + + return response diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 4830ecd518..b5a0f328da 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -14,6 +14,7 @@ from build.urls import build_urls from part.api import part_api_urls from company.api import company_api_urls from stock.api import stock_api_urls +from build.api import build_api_urls from django.conf import settings from django.conf.urls.static import static @@ -31,6 +32,7 @@ apipatterns = [ url(r'^part/', include(part_api_urls)), url(r'^company/', include(company_api_urls)), url(r'^stock/', include(stock_api_urls)), + url(r'^build/', include(build_api_urls)), # User URLs url(r'^user/', include(user_urls)), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 9d9536f886..6fc4302bfa 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -65,9 +65,7 @@ class AjaxMixin(object): else: return self.template_name - def renderJsonResponse(self, request, form, data={}): - - context = {} + def renderJsonResponse(self, request, form=None, data={}, context={}): if form: context['form'] = form @@ -92,22 +90,46 @@ class AjaxMixin(object): class AjaxView(AjaxMixin, View): """ Bare-bones AjaxView """ + # By default, point to the modal_form template + # (this can be overridden by a child class) + ajax_template_name = 'modal_form.html' + def post(self, request, *args, **kwargs): return JsonResponse('', safe=False) def get(self, request, *args, **kwargs): - return self.renderJsonResponse(request, None) + return self.renderJsonResponse(request) class AjaxCreateView(AjaxMixin, CreateView): + """ An 'AJAXified' CreateView for creating a new object in the db + - Returns a form in JSON format (for delivery to a modal window) + - Handles form validation via AJAX POST requests + """ + + def get(self, request, *args, **kwargs): + + response = super(CreateView, self).get(request, *args, **kwargs) + + if request.is_ajax(): + # Initialize a a new form + form = self.form_class(initial=self.get_initial()) + + return self.renderJsonResponse(request, form) + + else: + return response + def post(self, request, *args, **kwargs): form = self.form_class(data=request.POST, files=request.FILES) if request.is_ajax(): - data = {'form_valid': form.is_valid()} + data = { + 'form_valid': form.is_valid(), + } if form.is_valid(): obj = form.save() @@ -122,20 +144,25 @@ class AjaxCreateView(AjaxMixin, CreateView): else: return super(CreateView, self).post(request, *args, **kwargs) + +class AjaxUpdateView(AjaxMixin, UpdateView): + + """ An 'AJAXified' UpdateView for updating an object in the db + - Returns form in JSON format (for delivery to a modal window) + - Handles repeated form validation (via AJAX) until the form is valid + """ + def get(self, request, *args, **kwargs): - response = super(CreateView, self).get(request, *args, **kwargs) + html_response = super(UpdateView, self).get(request, *args, **kwargs) if request.is_ajax(): - form = self.form_class(initial=self.get_initial()) + form = self.form_class(instance=self.get_object()) return self.renderJsonResponse(request, form) else: - return response - - -class AjaxUpdateView(AjaxMixin, UpdateView): + return html_response def post(self, request, *args, **kwargs): @@ -154,45 +181,26 @@ class AjaxUpdateView(AjaxMixin, UpdateView): response = self.renderJsonResponse(request, form, data) return response - else: - return response - - def get(self, request, *args, **kwargs): - if request.is_ajax(): - form = self.form_class(instance=self.get_object()) - - return self.renderJsonResponse(request, form) - else: return super(UpdateView, self).post(request, *args, **kwargs) class AjaxDeleteView(AjaxMixin, DeleteView): - def post(self, request, *args, **kwargs): - - if request.is_ajax(): - obj = self.get_object() - pk = obj.id - obj.delete() - - data = {'id': pk, - 'delete': True} - - return self.renderJsonResponse(request, None, data) - - else: - return super(DeleteView, self).post(request, *args, **kwargs) + """ An 'AJAXified DeleteView for removing an object from the DB + - Returns a HTML object (not a form!) in JSON format (for delivery to a modal window) + - Handles deletion + """ def get(self, request, *args, **kwargs): - response = super(DeleteView, self).get(request, *args, **kwargs) + html_response = super(DeleteView, self).get(request, *args, **kwargs) if request.is_ajax(): data = {'id': self.get_object().id, - 'title': self.ajax_form_title, 'delete': False, + 'title': self.ajax_form_title, 'html_data': render_to_string(self.getAjaxTemplate(), self.get_context_data(), request=request) @@ -201,7 +209,23 @@ class AjaxDeleteView(AjaxMixin, DeleteView): return JsonResponse(data) else: - return response + return html_response + + def post(self, request, *args, **kwargs): + + if request.is_ajax(): + + obj = self.get_object() + pk = obj.id + obj.delete() + + data = {'id': pk, + 'delete': True} + + return self.renderJsonResponse(request, data=data) + + else: + return super(DeleteView, self).post(request, *args, **kwargs) class IndexView(TemplateView): diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py new file mode 100644 index 0000000000..41d46bab49 --- /dev/null +++ b/InvenTree/build/api.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters +from rest_framework import generics, permissions + +from django.conf.urls import url + +from .models import Build +from .serializers import BuildSerializer + + +class BuildList(generics.ListAPIView): + + queryset = Build.objects.all() + serializer_class = BuildSerializer + + permission_classes = [ + permissions.IsAuthenticatedOrReadOnly, + ] + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + + filter_fields = [ + 'part', + ] + + +build_api_urls = [ + url(r'^.*$', BuildList.as_view(), name='api-build-list') +] diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py new file mode 100644 index 0000000000..f28c9fcf89 --- /dev/null +++ b/InvenTree/build/serializers.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from rest_framework import serializers + +from .models import Build + + +class BuildSerializer(serializers.ModelSerializer): + + url = serializers.CharField(source='get_absolute_url', read_only=True) + + class Meta: + model = Build + fields = [ + 'pk', + 'url', + 'title', + 'creation_date', + 'completion_date', + 'part', + 'quantity', + 'notes'] diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index 81d2d0dbd4..553353ed4d 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -4,7 +4,11 @@

Part Builds

- +
+ +
+ +
@@ -20,9 +24,6 @@
Build
-
- -
{% include 'modals.html' %} diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index 901ba94293..c3d9f0a8b3 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -4,26 +4,21 @@ {% include 'company/tabs.html' with tab='parts' %} -
-
-

Supplier Parts

-
-
-

- - -

+

Supplier Parts

+ +
+ +

- +
{% endblock %} diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 90490bf7ad..bb2ee3413c 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -4,12 +4,12 @@ {% block content %} -
-

Companies

+
+

- +
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 024d61a4a5..bbb44a14e6 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -70,13 +70,14 @@ class PartList(generics.ListCreateAPIView): serializer_class = PartSerializer def get_queryset(self): - print("Get queryset") # Does the user wish to filter by category? cat_id = self.request.query_params.get('category', None) + # Start with all objects + parts_list = Part.objects.all() + if cat_id: - print("Getting category:", cat_id) category = get_object_or_404(PartCategory, pk=cat_id) # Filter by the supplied category @@ -90,10 +91,10 @@ class PartList(generics.ListCreateAPIView): continue flt |= Q(category=child) - return Part.objects.filter(flt) + parts_list = parts_list.filter(flt) # Default - return all parts - return Part.objects.all() + return parts_list permission_classes = [ permissions.IsAuthenticatedOrReadOnly, @@ -106,6 +107,11 @@ class PartList(generics.ListCreateAPIView): ] filter_fields = [ + 'buildable', + 'consumable', + 'trackable', + 'purchaseable', + 'salable', ] ordering_fields = [ diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index d0ad7571be..977d6e5ef3 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals from InvenTree.forms import HelperForm +from django import forms + from .models import Part, PartCategory, BomItem from .models import SupplierPart @@ -16,6 +18,27 @@ class PartImageForm(HelperForm): ] +class BomExportForm(HelperForm): + + # TODO - Define these choices somewhere else, and import them here + format_choices = ( + ('csv', 'CSV'), + ('pdf', 'PDF'), + ('xml', 'XML'), + ('xlsx', 'XLSX'), + ('html', 'HTML') + ) + + # Select export type + format = forms.CharField(label='Format', widget=forms.Select(choices=format_choices), required='true', help_text='Select export format') + + class Meta: + model = Part + fields = [ + 'format', + ] + + class EditPartForm(HelperForm): class Meta: @@ -28,8 +51,10 @@ class EditPartForm(HelperForm): 'URL', 'default_location', 'default_supplier', + 'units', 'minimum_stock', 'buildable', + 'consumable', 'trackable', 'purchaseable', 'salable', @@ -56,8 +81,10 @@ class EditBomItemForm(HelperForm): fields = [ 'part', 'sub_part', - 'quantity' + 'quantity', + 'note' ] + widgets = {'part': forms.HiddenInput()} class EditSupplierPartForm(HelperForm): diff --git a/InvenTree/part/migrations/0004_bomitem_note.py b/InvenTree/part/migrations/0004_bomitem_note.py new file mode 100644 index 0000000000..69bea83175 --- /dev/null +++ b/InvenTree/part/migrations/0004_bomitem_note.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-14 08:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0003_auto_20190412_2030'), + ] + + operations = [ + migrations.AddField( + model_name='bomitem', + name='note', + field=models.CharField(blank=True, help_text='Item notes', max_length=100), + ), + ] diff --git a/InvenTree/part/migrations/0005_part_consumable.py b/InvenTree/part/migrations/0005_part_consumable.py new file mode 100644 index 0000000000..4bb1056ae7 --- /dev/null +++ b/InvenTree/part/migrations/0005_part_consumable.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-15 13:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0004_bomitem_note'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='consumable', + field=models.BooleanField(default=False, help_text='Can this part be used to build other parts?'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index fe72814a32..306053762a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -126,9 +126,12 @@ class Part(models.Model): # Units of quantity for this part. Default is "pcs" units = models.CharField(max_length=20, default="pcs", blank=True) - # Can this part be built? + # Can this part be built from other parts? buildable = models.BooleanField(default=False, help_text='Can this part be built from other parts?') + # Can this part be used to make other parts? + consumable = models.BooleanField(default=True, help_text='Can this part be used to build other parts?') + # Is this part "trackable"? # Trackable parts can have unique instances # which are assigned serial numbers (or batch numbers) @@ -278,6 +281,73 @@ class Part(models.Model): # Return the number of supplier parts available for this part return self.supplier_parts.count() + def export_bom(self, **kwargs): + + # Construct the export data + header = [] + header.append('Part') + header.append('Description') + header.append('Quantity') + header.append('Note') + + rows = [] + + for it in self.bom_items.all(): + line = [] + + line.append(it.sub_part.name) + line.append(it.sub_part.description) + line.append(it.quantity) + line.append(it.note) + + rows.append([str(x) for x in line]) + + file_format = kwargs.get('format', 'csv').lower() + + kwargs['header'] = header + kwargs['rows'] = rows + + if file_format == 'csv': + return self.export_bom_csv(**kwargs) + elif file_format in ['xls', 'xlsx']: + return self.export_bom_xls(**kwargs) + elif file_format == 'xml': + return self.export_bom_xml(**kwargs) + elif file_format in ['htm', 'html']: + return self.export_bom_htm(**kwargs) + elif file_format == 'pdf': + return self.export_bom_pdf(**kwargs) + else: + return None + + def export_bom_csv(self, **kwargs): + + # Construct header line + header = kwargs.get('header') + rows = kwargs.get('rows') + + # TODO - Choice of formatters goes here? + out = ','.join(header) + + for row in rows: + out += '\n' + out += ','.join(row) + + return out + + def export_bom_xls(self, **kwargs): + + return '' + + def export_bom_xml(self, **kwargs): + return '' + + def export_bom_htm(self, **kwargs): + return '' + + def export_bom_pdf(self, **kwargs): + return '' + """ @property def projects(self): @@ -338,11 +408,15 @@ class BomItem(models.Model): # A link to the child item (sub-part) # Each part will get a reverse lookup field 'used_in' - sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in') + sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in', + limit_choices_to={'consumable': True}) # Quantity required quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)]) + # Note attached to this BOM line item + note = models.CharField(max_length=100, blank=True, help_text='Item notes') + def clean(self): # A part cannot refer to itself in its BOM diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 5266321963..5e2596d126 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -43,7 +43,7 @@ class PartSerializer(serializers.ModelSerializer): """ url = serializers.CharField(source='get_absolute_url', read_only=True) - category = CategorySerializer(many=False, read_only=True) + category_name = serializers.CharField(source='category_path', read_only=True) class Meta: model = Part @@ -55,11 +55,13 @@ class PartSerializer(serializers.ModelSerializer): 'URL', # Link to an external URL (optional) 'description', 'category', + 'category_name', 'total_stock', 'available_stock', 'units', 'trackable', 'buildable', + 'consumable', 'trackable', 'salable', ] @@ -79,7 +81,8 @@ class BomItemSerializer(serializers.ModelSerializer): 'url', 'part', 'sub_part', - 'quantity' + 'quantity', + 'note', ] diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 7b9e829686..805f501dcd 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -11,109 +11,72 @@

Bill of Materials

- -
- -
- +
+ {% if editing_enabled %} +
+ + +
+ {% else %} + + {% endif %}
+ +
+ {% endblock %} + +{% block js_load %} +{{ block.super }} + + + +{% endblock %} + {% block js_ready %} {{ block.super }} - function reloadBom() { - $("#bom-table").bootstrapTable('refresh'); - } - $('#bom-table').on('click', '.delete-button', function () { - var button = $(this); - - launchDeleteForm( - button.attr('url'), - { - success: reloadBom - }); + // Load the BOM table data + loadBomTable($("#bom-table"), { + editable: {{ editing_enabled }}, + bom_url: "{% url 'api-bom-list' %}", + part_url: "{% url 'api-part-list' %}", + parent_id: {{ part.id }} }); - $("#bom-table").on('click', '.edit-button', function () { - var button = $(this); + {% if editing_enabled %} + $("#editing-finished").click(function() { + location.href = "{% url 'part-bom' part.id %}"; + }); - launchModalForm( - button.attr('url'), - { - success: reloadBom - }); + $("#bom-item-new").click(function () { + launchModalForm("{% url 'bom-item-create' %}?parent={{ part.id }}", {}); + }); + + {% else %} + + $("#edit-bom").click(function () { + location.href = "{% url 'part-bom' part.id %}?edit=True"; + }); + + $("#export-bom").click(function () { + downloadBom({ + modal: '#modal-form', + url: "{% url 'bom-export' part.id %}" + }); }); - $("#new-bom-item").click(function () { - launchModalForm( - "{% url 'bom-item-create' %}", - { - reload: true, - data: { - parent: {{ part.id }} - } - }); - }); + {% endif %} - $("#bom-table").bootstrapTable({ - sortable: true, - search: true, - queryParams: function(p) { - return { - part: {{ part.id }} - } - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - }, - { - field: 'sub_part', - title: 'Part', - sortable: true, - formatter: function(value, row, index, field) { - return renderLink(value.name, value.url); - } - }, - { - field: 'sub_part.description', - title: 'Description', - }, - { - field: 'quantity', - title: 'Required', - searchable: false, - sortable: true - }, - { - field: 'sub_part.available_stock', - title: 'Available', - searchable: false, - sortable: true, - formatter: function(value, row, index, field) { - var text = ""; - if (row.quantity < row.sub_part.available_stock) - { - text = "" + value + ""; - } - else - { - text = "" + value + ""; - } - - return renderLink(text, row.sub_part.url + "stock/"); - } - }, - { - formatter: function(value, row, index, field) { - return editButton(row.url + 'edit') + ' ' + deleteButton(row.url + 'delete'); - } - } - ], - url: "{% url 'api-bom-list' %}" - }); {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index acaa70a60c..999fe7e231 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -6,34 +6,13 @@

Part Builds

- - - - - - - - -{% if part.active_builds|length > 0 %} - - - -{% include "part/build_list.html" with builds=part.active_builds %} -{% endif %} - -{% if part.inactive_builds|length > 0 %} - - - - -{% include "part/build_list.html" with builds=part.inactive_builds %} -{% endif %} +
+ +
+
TitleQuantityStatusCompletion Date
Active Builds
Inactive Builds
-
- -
{% endblock %} @@ -49,4 +28,43 @@ } }); }); + + $("#build-table").bootstrapTable({ + sortable: true, + search: true, + pagination: true, + queryParams: function(p) { + return { + part: {{ part.id }}, + } + }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + }, + { + field: 'title', + title: 'Title', + formatter: function(value, row, index, field) { + return renderLink(value, row.url); + } + }, + { + field: 'quantity', + title: 'Quantity', + }, + { + field: 'status', + title: 'Status', + }, + { + field: 'completion_date', + title: 'Completed' + } + ], + url: "{% url 'api-build-list' %}", + }); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 111efe7fcf..cc10828f80 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -40,13 +40,13 @@ {% endif %}
- -
- -
- +
+
+ +
+ {% endblock %} {% block js_load %} {{ block.super }} @@ -151,11 +151,11 @@ }, { sortable: true, - field: 'category', + field: 'category_name', title: 'Category', formatter: function(value, row, index, field) { if (row.category) { - return renderLink(row.category.pathstring, row.category.url); + return renderLink(row.category_name, "/part/category/" + row.category + "/"); } else { return ''; diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index c43aec77eb..b340ec35c5 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -32,7 +32,7 @@ Description - {{ part.decription }} + {{ part.description }} {% if part.IPN %} @@ -44,7 +44,7 @@ Category {% if part.category %} - {{ part.category.name }} + {{ part.category.pathstring }} {% endif %} @@ -70,6 +70,10 @@ Buildable {% include "yesnolabel.html" with value=part.buildable %} + + Consumable + {% include "yesnolabel.html" with value=part.consumable %} + Trackable {% include "yesnolabel.html" with value=part.trackable %} diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index f7314a41a3..bd6859777e 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -37,7 +37,7 @@
-

Stock Status - {{ part.available_stock }} available

+

Stock Status - {{ part.available_stock }}{% if part.units %} {{ part.units }} {% endif%} available

diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html index f86822f53c..96fd03e3cd 100644 --- a/InvenTree/part/templates/part/stock.html +++ b/InvenTree/part/templates/part/stock.html @@ -4,32 +4,23 @@ {% include 'part/tabs.html' with tab='stock' %} -
-
-

Part Stock

-
-
-

-
- - -
-

+

Part Stock

+ +
+ +
-
- -
In Stock
+
diff --git a/InvenTree/part/templates/part/subcategories.html b/InvenTree/part/templates/part/subcategories.html index 4f8704e736..5dcfa426ad 100644 --- a/InvenTree/part/templates/part/subcategories.html +++ b/InvenTree/part/templates/part/subcategories.html @@ -4,7 +4,7 @@
diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index 13c745e33a..b5426971eb 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -4,20 +4,15 @@ {% include 'part/tabs.html' with tab='suppliers' %} -
-
-

Part Suppliers

-
-
-

- -

-
+

Part Suppliers

+ +
+

- +
{% endblock %} diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 3c5773b8e2..aa9d798bb7 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -7,7 +7,7 @@ Build{{ part.active_builds|length }} {% endif %} - {% if part.used_in_count > 0 %} + {% if part.consumable 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/urls.py b/InvenTree/part/urls.py index ab680e4b52..75d5041b9c 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -19,6 +19,7 @@ part_detail_urls = [ url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), + url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 9bb42722eb..8bdeaea8bb 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy - from django.views.generic import DetailView, ListView from company.models import Company @@ -15,10 +14,13 @@ from .forms import PartImageForm from .forms import EditPartForm from .forms import EditCategoryForm from .forms import EditBomItemForm +from .forms import BomExportForm from .forms import EditSupplierPartForm -from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView + +from InvenTree.helpers import DownloadFile class PartIndex(ListView): @@ -88,6 +90,17 @@ class PartDetail(DetailView): queryset = Part.objects.all() template_name = 'part/detail.html' + # Add in some extra context information based on query params + def get_context_data(self, **kwargs): + context = super(PartDetail, self).get_context_data(**kwargs) + + if self.request.GET.get('edit', '').lower() in ['true', 'yes', '1']: + context['editing_enabled'] = 1 + else: + context['editing_enabled'] = 0 + + return context + class PartImage(AjaxUpdateView): @@ -104,10 +117,88 @@ class PartImage(AjaxUpdateView): class PartEdit(AjaxUpdateView): model = Part - form_class = EditPartForm template_name = 'part/edit.html' + form_class = EditPartForm ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit Part Properties' + context_object_name = 'part' + + +class BomExport(AjaxView): + + model = Part + ajax_form_title = 'Export BOM' + ajax_template_name = 'part/bom_export.html' + context_object_name = 'part' + form_class = BomExportForm + + def get_object(self): + return get_object_or_404(Part, pk=self.kwargs['pk']) + + def get(self, request, *args, **kwargs): + form = self.form_class() + + """ + part = self.get_object() + + context = { + 'part': part + } + + if request.is_ajax(): + passs + """ + + return self.renderJsonResponse(request, form) + + def post(self, request, *args, **kwargs): + """ + User has now submitted the BOM export data + """ + + # part = self.get_object() + + return super(AjaxView, self).post(request, *args, **kwargs) + + def get_data(self): + return { + # 'form_valid': True, + # 'redirect': '/' + # 'redirect': reverse('bom-download', kwargs={'pk': self.request.GET.get('pk')}) + } + + +class BomDownload(AjaxView): + """ + Provide raw download of a BOM file. + - File format should be passed as a query param e.g. ?format=csv + """ + + # TODO - This should no longer extend an AjaxView! + + model = Part + # form_class = BomExportForm + # template_name = 'part/bom_export.html' + # ajax_form_title = 'Export Bill of Materials' + # context_object_name = 'part' + + def get(self, request, *args, **kwargs): + + part = get_object_or_404(Part, pk=self.kwargs['pk']) + + export_format = request.GET.get('format', 'csv') + + # Placeholder to test file export + filename = '"' + part.name + '_BOM.' + export_format + '"' + + filedata = part.export_bom(format=export_format) + + return DownloadFile(filedata, filename) + + def get_data(self): + return { + 'info': 'Exported BOM' + } class PartDelete(AjaxDeleteView): @@ -115,6 +206,7 @@ class PartDelete(AjaxDeleteView): template_name = 'part/delete.html' ajax_template_name = 'part/partial_delete.html' ajax_form_title = 'Confirm Part Deletion' + context_object_name = 'part' success_url = '/part/' diff --git a/InvenTree/static/css/select2-bootstrap.css b/InvenTree/static/css/select2-bootstrap.css new file mode 100644 index 0000000000..50c94c4e1f --- /dev/null +++ b/InvenTree/static/css/select2-bootstrap.css @@ -0,0 +1,4052 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select2-bootstrap-theme/select2-bootstrap.css at master · select2/select2-bootstrap-theme + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Skip to content +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +
+
+
+ + + + + + + + + + + + +
+
+ +
    + + + +
  • + +
    + +
    + + + Watch + + +
    + Notifications +
    +
    + + + + + + + + +
    +
    +
    + +
    +
  • + +
  • +
    +
    + + +
    +
    + + +
    + +
  • + +
  • +
    + + Fork + +
    + +

    Fork select2-bootstrap-theme

    +
    +
    + +
    +

    If this dialog fails to load, you can visit the fork page directly.

    +
    +
    +
    +
    + + +
  • +
+ +

+ + /select2-bootstrap-theme + +

+ +
+ + + + +
+
+
+ + + + + + + + Permalink + + + + + +
+ + +
+ + Branch: + master + + + + + + + +
+ +
+ + Find file + + + Copy path + +
+
+ + +
+ + Find file + + + Copy path + +
+
+ + + + +
+
+ + @fk + fk + + 0.1.0-beta.10 + + + + 87f8621 + Mar 30, 2017 + +
+ +
+
+ + 1 contributor + + +
+ +

+ Users who have contributed to this file +

+
+ + +
+
+
+
+ + + + + +
+ +
+ +
+ 722 lines (625 sloc) + + 22.6 KB +
+ +
+ +
+ Raw + Blame + History +
+ + +
+ + + + +
+ +
+
+ +
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
/*!
* Select2 Bootstrap Theme v0.1.0-beta.10 (https://select2.github.io/select2-bootstrap-theme)
* Copyright 2015-2017 Florian Kissling and contributors (https://github.com/select2/select2-bootstrap-theme/graphs/contributors)
* Licensed under MIT (https://github.com/select2/select2-bootstrap-theme/blob/master/LICENSE)
*/
+
.select2-container--bootstrap {
display: block;
/*------------------------------------* #COMMON STYLES
\*------------------------------------*/
/**
* Search field in the Select2 dropdown.
*/
/**
* No outline for all search fields - in the dropdown
* and inline in multi Select2s.
*/
/**
* Adjust Select2's choices hover and selected styles to match
* Bootstrap 3's default dropdown styles.
*
* @see http://getbootstrap.com/components/#dropdowns
*/
/**
* Clear the selection.
*/
/**
* Address disabled Select2 styles.
*
* @see https://select2.github.io/examples.html#disabled
* @see http://getbootstrap.com/css/#forms-control-disabled
*/
/*------------------------------------* #DROPDOWN
\*------------------------------------*/
/**
* Dropdown border color and box-shadow.
*/
/**
* Limit the dropdown height.
*/
/*------------------------------------* #SINGLE SELECT2
\*------------------------------------*/
/*------------------------------------* #MULTIPLE SELECT2
\*------------------------------------*/
/**
* Address Bootstrap control sizing classes
*
* 1. Reset Bootstrap defaults.
* 2. Adjust the dropdown arrow button icon position.
*
* @see http://getbootstrap.com/css/#forms-control-sizes
*/
/* 1 */
/*------------------------------------* #RTL SUPPORT
\*------------------------------------*/
}
+
.select2-container--bootstrap .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
color: #555555;
font-size: 14px;
outline: 0;
}
+
.select2-container--bootstrap .select2-selection.form-control {
border-radius: 4px;
}
+
.select2-container--bootstrap .select2-search--dropdown .select2-search__field {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
color: #555555;
font-size: 14px;
}
+
.select2-container--bootstrap .select2-search__field {
outline: 0;
/* Firefox 18- */
/**
* Firefox 19+
*
* @see http://stackoverflow.com/questions/24236240/color-for-styled-placeholder-text-is-muted-in-firefox
*/
}
+
.select2-container--bootstrap .select2-search__field::-webkit-input-placeholder {
color: #999;
}
+
.select2-container--bootstrap .select2-search__field:-moz-placeholder {
color: #999;
}
+
.select2-container--bootstrap .select2-search__field::-moz-placeholder {
color: #999;
opacity: 1;
}
+
.select2-container--bootstrap .select2-search__field:-ms-input-placeholder {
color: #999;
}
+
.select2-container--bootstrap .select2-results__option {
padding: 6px 12px;
/**
* Disabled results.
*
* @see https://select2.github.io/examples.html#disabled-results
*/
/**
* Hover state.
*/
/**
* Selected state.
*/
}
+
.select2-container--bootstrap .select2-results__option[role=group] {
padding: 0;
}
+
.select2-container--bootstrap .select2-results__option[aria-disabled=true] {
color: #777777;
cursor: not-allowed;
}
+
.select2-container--bootstrap .select2-results__option[aria-selected=true] {
background-color: #f5f5f5;
color: #262626;
}
+
.select2-container--bootstrap .select2-results__option--highlighted[aria-selected] {
background-color: #337ab7;
color: #fff;
}
+
.select2-container--bootstrap .select2-results__option .select2-results__option {
padding: 6px 12px;
}
+
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
+
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option {
margin-left: -12px;
padding-left: 24px;
}
+
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -24px;
padding-left: 36px;
}
+
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -36px;
padding-left: 48px;
}
+
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -48px;
padding-left: 60px;
}
+
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -60px;
padding-left: 72px;
}
+
.select2-container--bootstrap .select2-results__group {
color: #777777;
display: block;
padding: 6px 12px;
font-size: 12px;
line-height: 1.42857143;
white-space: nowrap;
}
+
.select2-container--bootstrap.select2-container--focus .select2-selection, .select2-container--bootstrap.select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-webkit-transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
border-color: #66afe9;
}
+
.select2-container--bootstrap.select2-container--open {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
/**
* Handle border radii of the container when the dropdown is showing.
*/
}
+
.select2-container--bootstrap.select2-container--open .select2-selection .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 4px 4px 4px;
}
+
.select2-container--bootstrap.select2-container--open.select2-container--below .select2-selection {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-color: transparent;
}
+
.select2-container--bootstrap.select2-container--open.select2-container--above .select2-selection {
border-top-right-radius: 0;
border-top-left-radius: 0;
border-top-color: transparent;
}
+
.select2-container--bootstrap .select2-selection__clear {
color: #999;
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px;
}
+
.select2-container--bootstrap .select2-selection__clear:hover {
color: #333;
}
+
.select2-container--bootstrap.select2-container--disabled .select2-selection {
border-color: #ccc;
-webkit-box-shadow: none;
box-shadow: none;
}
+
.select2-container--bootstrap.select2-container--disabled .select2-selection,
.select2-container--bootstrap.select2-container--disabled .select2-search__field {
cursor: not-allowed;
}
+
.select2-container--bootstrap.select2-container--disabled .select2-selection,
.select2-container--bootstrap.select2-container--disabled .select2-selection--multiple .select2-selection__choice {
background-color: #eeeeee;
}
+
.select2-container--bootstrap.select2-container--disabled .select2-selection__clear,
.select2-container--bootstrap.select2-container--disabled .select2-selection--multiple .select2-selection__choice__remove {
display: none;
}
+
.select2-container--bootstrap .select2-dropdown {
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
border-color: #66afe9;
overflow-x: hidden;
margin-top: -1px;
}
+
.select2-container--bootstrap .select2-dropdown--above {
-webkit-box-shadow: 0px -6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0px -6px 12px rgba(0, 0, 0, 0.175);
margin-top: 1px;
}
+
.select2-container--bootstrap .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
}
+
.select2-container--bootstrap .select2-selection--single {
height: 34px;
line-height: 1.42857143;
padding: 6px 24px 6px 12px;
/**
* Adjust the single Select2's dropdown arrow button appearance.
*/
}
+
.select2-container--bootstrap .select2-selection--single .select2-selection__arrow {
position: absolute;
bottom: 0;
right: 12px;
top: 0;
width: 4px;
}
+
.select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
border-color: #999 transparent transparent transparent;
border-style: solid;
border-width: 4px 4px 0 4px;
height: 0;
left: 0;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
+
.select2-container--bootstrap .select2-selection--single .select2-selection__rendered {
color: #555555;
padding: 0;
}
+
.select2-container--bootstrap .select2-selection--single .select2-selection__placeholder {
color: #999;
}
+
.select2-container--bootstrap .select2-selection--multiple {
min-height: 34px;
padding: 0;
height: auto;
/**
* Make Multi Select2's choices match Bootstrap 3's default button styles.
*/
/**
* Minus 2px borders.
*/
/**
* Clear the selection.
*/
}
+
.select2-container--bootstrap .select2-selection--multiple .select2-selection__rendered {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
display: block;
line-height: 1.42857143;
list-style: none;
margin: 0;
overflow: hidden;
padding: 0;
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
}
+
.select2-container--bootstrap .select2-selection--multiple .select2-selection__placeholder {
color: #999;
float: left;
margin-top: 5px;
}
+
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
color: #555555;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
cursor: default;
float: left;
margin: 5px 0 0 6px;
padding: 0 6px;
}
+
.select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
background: transparent;
padding: 0 12px;
height: 32px;
line-height: 1.42857143;
margin-top: 0;
min-width: 5em;
}
+
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 3px;
}
+
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333;
}
+
.select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 6px;
}
+
.select2-container--bootstrap .select2-selection--single.input-sm,
.input-group-sm .select2-container--bootstrap .select2-selection--single,
.form-group-sm .select2-container--bootstrap .select2-selection--single {
border-radius: 3px;
font-size: 12px;
height: 30px;
line-height: 1.5;
padding: 5px 22px 5px 10px;
/* 2 */
}
+
.select2-container--bootstrap .select2-selection--single.input-sm .select2-selection__arrow b,
.input-group-sm .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b,
.form-group-sm .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
margin-left: -5px;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-sm,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple {
min-height: 30px;
border-radius: 3px;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-selection__choice,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
font-size: 12px;
line-height: 1.5;
margin: 4px 0 0 5px;
padding: 0 5px;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-search--inline .select2-search__field,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
padding: 0 10px;
font-size: 12px;
height: 28px;
line-height: 1.5;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-selection__clear,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 5px;
}
+
.select2-container--bootstrap .select2-selection--single.input-lg,
.input-group-lg .select2-container--bootstrap .select2-selection--single,
.form-group-lg .select2-container--bootstrap .select2-selection--single {
border-radius: 6px;
font-size: 18px;
height: 46px;
line-height: 1.3333333;
padding: 10px 31px 10px 16px;
/* 1 */
}
+
.select2-container--bootstrap .select2-selection--single.input-lg .select2-selection__arrow,
.input-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow,
.form-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow {
width: 5px;
}
+
.select2-container--bootstrap .select2-selection--single.input-lg .select2-selection__arrow b,
.input-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b,
.form-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
border-width: 5px 5px 0 5px;
margin-left: -5px;
margin-left: -10px;
margin-top: -2.5px;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-lg,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple {
min-height: 46px;
border-radius: 6px;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-selection__choice,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
font-size: 18px;
line-height: 1.3333333;
border-radius: 4px;
margin: 9px 0 0 8px;
padding: 0 10px;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-search--inline .select2-search__field,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
padding: 0 16px;
font-size: 18px;
height: 44px;
line-height: 1.3333333;
}
+
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-selection__clear,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 10px;
}
+
.select2-container--bootstrap .select2-selection.input-lg.select2-container--open .select2-selection--single {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
}
+
.select2-container--bootstrap .select2-selection.input-lg.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 5px 5px 5px;
}
+
.input-group-lg .select2-container--bootstrap .select2-selection.select2-container--open .select2-selection--single {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
}
+
.input-group-lg .select2-container--bootstrap .select2-selection.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 5px 5px 5px;
}
+
.select2-container--bootstrap[dir="rtl"] {
/**
* Single Select2
*
* 1. Makes sure that .select2-selection__placeholder is positioned
* correctly.
*/
/**
* Multiple Select2
*/
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--single {
padding-left: 24px;
padding-right: 12px;
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 0;
padding-left: 0;
text-align: right;
/* 1 */
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 12px;
right: auto;
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__arrow b {
margin-left: 0;
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice,
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 0;
margin-right: 6px;
}
+
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
+
/*------------------------------------* #ADDITIONAL GOODIES
\*------------------------------------*/
/**
* Address Bootstrap's validation states
*
* If a Select2 widget parent has one of Bootstrap's validation state modifier
* classes, adjust Select2's border colors and focus states accordingly.
* You may apply said classes to the Select2 dropdown (body > .select2-container)
* via JavaScript match Bootstraps' to make its styles match.
*
* @see http://getbootstrap.com/css/#forms-control-validation
*/
.has-warning .select2-dropdown,
.has-warning .select2-selection {
border-color: #8a6d3b;
}
+
.has-warning .select2-container--focus .select2-selection,
.has-warning .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
border-color: #66512c;
}
+
.has-warning.select2-drop-active {
border-color: #66512c;
}
+
.has-warning.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #66512c;
}
+
.has-error .select2-dropdown,
.has-error .select2-selection {
border-color: #a94442;
}
+
.has-error .select2-container--focus .select2-selection,
.has-error .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
border-color: #843534;
}
+
.has-error.select2-drop-active {
border-color: #843534;
}
+
.has-error.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #843534;
}
+
.has-success .select2-dropdown,
.has-success .select2-selection {
border-color: #3c763d;
}
+
.has-success .select2-container--focus .select2-selection,
.has-success .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
border-color: #2b542c;
}
+
.has-success.select2-drop-active {
border-color: #2b542c;
}
+
.has-success.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #2b542c;
}
+
/**
* Select2 widgets in Bootstrap Input Groups
*
* @see http://getbootstrap.com/components/#input-groups
* @see https://github.com/twbs/bootstrap/blob/master/less/input-groups.less
*/
/**
* Reset rounded corners
*/
.input-group > .select2-hidden-accessible:first-child + .select2-container--bootstrap > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:first-child + .select2-container--bootstrap > .selection > .select2-selection.form-control {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
+
.input-group > .select2-hidden-accessible:not(:first-child) + .select2-container--bootstrap:not(:last-child) > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:not(:first-child) + .select2-container--bootstrap:not(:last-child) > .selection > .select2-selection.form-control {
border-radius: 0;
}
+
.input-group > .select2-hidden-accessible:not(:first-child):not(:last-child) + .select2-container--bootstrap:last-child > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:not(:first-child):not(:last-child) + .select2-container--bootstrap:last-child > .selection > .select2-selection.form-control {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
+
.input-group > .select2-container--bootstrap {
display: table;
table-layout: fixed;
position: relative;
z-index: 2;
width: 100%;
margin-bottom: 0;
/**
* Adjust z-index like Bootstrap does to show the focus-box-shadow
* above appended buttons in .input-group and .form-group.
*/
/**
* Adjust alignment of Bootstrap buttons in Bootstrap Input Groups to address
* Multi Select2's height which - depending on how many elements have been selected -
* may grow taller than its initial size.
*
* @see http://getbootstrap.com/components/#input-groups
*/
}
+
.input-group > .select2-container--bootstrap > .selection > .select2-selection.form-control {
float: none;
}
+
.input-group > .select2-container--bootstrap.select2-container--open, .input-group > .select2-container--bootstrap.select2-container--focus {
z-index: 3;
}
+
.input-group > .select2-container--bootstrap,
.input-group > .select2-container--bootstrap .input-group-btn,
.input-group > .select2-container--bootstrap .input-group-btn .btn {
vertical-align: top;
}
+
/**
* Temporary fix for https://github.com/select2/select2-bootstrap-theme/issues/9
*
* Provides `!important` for certain properties of the class applied to the
* original `<select>` element to hide it.
*
* @see https://github.com/select2/select2/pull/3301
* @see https://github.com/fk/select2/commit/31830c7b32cb3d8e1b12d5b434dee40a6e753ada
*/
.form-control.select2-hidden-accessible {
position: absolute !important;
width: 1px !important;
}
+
/**
* Display override for inline forms
*/
@media (min-width: 768px) {
.form-inline .select2-container--bootstrap {
display: inline-block;
}
}
+ + + +
+ +
+ + + +
+ + +
+ + +
+
+ + + +
+ +
+ +
+
+ + +
+ + + + + + +
+ + + You can’t perform that action at this time. +
+ + + + + + + + + + + + + + +
+ + + + diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js new file mode 100644 index 0000000000..c3e7de0351 --- /dev/null +++ b/InvenTree/static/script/inventree/bom.js @@ -0,0 +1,192 @@ +/* BOM management functions. + * Requires follwing files to be loaded first: + * - api.js + * - part.js + * - modals.js + */ + + +function reloadBomTable(table, options) { + + table.bootstrapTable('refresh'); +} + + +function downloadBom(options = {}) { + + var modal = options.modal || "#modal-form"; + + var content = ` + Select file format
+
+ +
+ `; + + openModal({ + modal: modal, + title: "Export Bill of Materials", + submit_text: "Download", + close_text: "Cancel", + }); + + modalSetContent(modal, content); + + $(modal).on('click', '#modal-form-submit', function() { + $(modal).modal('hide'); + + var format = $(modal).find('#bom-format :selected').val(); + + if (options.url) { + var url = options.url + "?format=" + format; + + location.href = url; + } + }); +} + + +function loadBomTable(table, options) { + /* Load a BOM table with some configurable options. + * + * Following options are available: + * editable - Should the BOM table be editable? + * bom_url - Address to request BOM data from + * part_url - Address to request Part data from + * parent_id - Parent ID of the owning part + * + * BOM data are retrieved from the server via AJAX query + */ + + // Construct the table columns + + var cols = [ + { + field: 'pk', + title: 'ID', + visible: false, + } + ]; + + if (options.editable) { + cols.push({ + formatter: function(value, row, index, field) { + var bEdit = ""; + var bDelt = ""; + + return "
" + bEdit + bDelt + "
"; + } + }); + } + + // Part column + cols.push( + { + field: 'sub_part', + title: 'Part', + sortable: true, + formatter: function(value, row, index, field) { + return renderLink(value.name, value.url); + } + } + ); + + // Part description + cols.push( + { + field: 'sub_part.description', + title: 'Description', + } + ); + + // Part quantity + cols.push( + { + field: 'quantity', + title: 'Required', + searchable: false, + sortable: true, + } + ); + + // Part notes + cols.push( + { + field: 'note', + title: 'Notes', + searchable: true, + sortable: false, + } + ); + + // If we are NOT editing, display the available stock + if (!options.editable) { + cols.push( + { + field: 'sub_part.available_stock', + title: 'Available', + searchable: false, + sortable: true, + formatter: function(value, row, index, field) { + var text = ""; + + if (row.quantity < row.sub_part.available_stock) + { + text = "" + value + ""; + } + else + { + text = "" + value + ""; + } + + return renderLink(text, row.sub_part.url + "stock/"); + } + } + ); + } + + // Configure the table (bootstrap-table) + + table.bootstrapTable({ + sortable: true, + search: true, + clickToSelect: true, + queryParams: function(p) { + return { + part: options.parent_id, + } + }, + columns: cols, + url: options.bom_url + }); + + // In editing mode, attached editables to the appropriate table elements + if (options.editable) { + + table.on('click', '.bom-delete-button', function() { + var button = $(this); + + launchDeleteForm(button.attr('url'), { + success: function() { + reloadBomTable(table); + } + }); + }); + + table.on('click', '.bom-edit-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + success: function() { + reloadBomTable(table); + } + }); + }); + } +} \ No newline at end of file diff --git a/InvenTree/static/script/tables.js b/InvenTree/static/script/tables.js index 1510f48cbe..c6ff11525a 100644 --- a/InvenTree/static/script/tables.js +++ b/InvenTree/static/script/tables.js @@ -14,5 +14,49 @@ function renderLink(text, url) { return '' + text + ''; } +function renderEditable(text, options) { + /* Wrap the text in an 'editable' link + * (using bootstrap-editable library) + * + * Can pass the following parameters in 'options': + * _type - parameter for data-type (default = 'text') + * _pk - parameter for data-pk (required) + * _title - title to show when editing + * _empty - placeholder text to show when field is empty + * _class - html class (default = 'editable-item') + * _id - id + * _value - Initial value of the editable (default = blank) + */ + // Default values (if not supplied) + var _type = options._type || 'text'; + var _class = options._class || 'editable-item'; + + var html = "" + text + ""; + + return html; +} diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 15e84a88a1..5a232ec74f 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -38,23 +38,26 @@
- -
- -
- -