diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 5770777d28..e3119e6fdb 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -111,8 +111,8 @@ src="{% static 'img/blank_image.png' %}"
  • {% trans "Cancel Build" %}
  • {% endif %} {% if build.status == BuildStatus.CANCELLED and roles.build.delete %} -
  • {% trans "Delete Build"% } - {% endif %} +
  • {% trans "Delete Build" %} + {% endif %} {% endif %} diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 3612a1c9f9..c0d049ecc7 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -443,6 +443,8 @@ class PartFilter(rest_filters.FilterSet): else: queryset = queryset.filter(IPN='') + return queryset + # Regex filter for name name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex') diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 00d7f01e47..59aec17944 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -899,7 +899,7 @@ {% for line in price_history %}'{{ line.date }}',{% endfor %} ], datasets: [{ - label: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}', + label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}', backgroundColor: 'rgba(255, 99, 132, 0.2)', borderColor: 'rgb(255, 99, 132)', yAxisID: 'y', @@ -911,7 +911,7 @@ }, {% if 'price_diff' in price_history.0 %} { - label: '{% blocktrans %}Single Price Difference - {{currency}}{% endblocktrans %}', + label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}', backgroundColor: 'rgba(68, 157, 68, 0.2)', borderColor: 'rgb(68, 157, 68)', yAxisID: 'y2', @@ -923,7 +923,7 @@ hidden: true, }, { - label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}', + label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}', backgroundColor: 'rgba(70, 127, 155, 0.2)', borderColor: 'rgb(70, 127, 155)', yAxisID: 'y', diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index d7b196917d..5191399f0a 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -10,132 +10,253 @@ {% block content %} -
    +
    + +

    {{ part.full_name }}

    +
    +
    +
    + {% include "part/part_thumb.html" %} +
    +

    +

    + + {% if user.is_staff and roles.part.change %} +   + {% endif %} + +
    + {% if part.is_template %} + + {% endif %} + {% if part.assembly %} + + {% endif %} + {% if part.component %} + + {% endif %} + {% if part.trackable %} + + {% endif %} + {% if part.purchaseable %} + + {% endif %} + {% if part.salable %} + + {% endif %} +
    + + {% if not part.active %} +   +
    + + {% trans 'Inactive' %} +
    + {% endif %} + + {% if part.virtual and part.active %} +   +
    + + {% trans 'Virtual' %} +
    + {% endif %} +

    +

    -
    -
    - {% include "part/part_thumb.html" %} -
    -

    - {{ part.full_name }} - {% if user.is_staff and roles.part.change %} - - {% endif %} - {% if not part.active %} -
    - {% trans 'Inactive' %} -
    - {% endif %} -

    - {% if part.description %} -

    {{ part.description }}

    - {% endif %} -

    -

    - {% if part.virtual %} - +
    + + + {% if barcodes %} + + {% endif %} - {% if part.is_template %} - - {% endif %} - {% if part.assembly %} - - {% endif %} - {% if part.component %} - - {% endif %} - {% if part.trackable %} - + {% if part.active %} + + {% if roles.stock.change %} + {% endif %} {% if part.purchaseable %} - - {% endif %} - {% if part.salable %} - - {% endif %} -
    -

    - -
    - - - {% if barcodes %} - - - {% endif %} - {% if part.active %} - - {% if roles.stock.change %} -
    - - + {% endif %} + {% endif %} + {% endif %} + + {% if roles.part.add or roles.part.change or roles.part.delete %} +
    + + +
    + {% endif %}
    - {% endif %} - {% if part.purchaseable %} - {% if roles.purchase_order.add %} - - {% endif %} - {% endif %} - {% endif %} - - {% if roles.part.add or roles.part.change or roles.part.delete %} -
    - - +
    +
    +
    + {% if part.variant_of %} +
    + {% object_link 'part-detail' part.variant_of.id part.variant_of.full_name as link %} + {% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
    {% endif %}
    - + +
    +
    + + + + + + + + + + + + {% if on_order > 0 %} + + + + + + {% endif %} + {% if required_build_order_quantity > 0 %} + + + + + {% endif %} + {% if required_sales_order_quantity > 0 %} + + + + + {% endif %} + {% if allocated > 0 %} + + + + + + {% endif %} + + {% if not part.is_template %} + {% if part.assembly %} + + + + + + + + + + {% if quantity_being_built > 0 %} + + + + + + {% endif %} + {% endif %} + {% endif %} +

    {% trans "Available Stock" %}

    {% decimal available %}{% if part.units %} {{ part.units }}{% endif %}

    {% trans "In Stock" %}{% include "part/stock_count.html" %}
    {% trans "On Order" %}{% decimal on_order %}
    {% trans "Required for Build Orders" %}{% decimal required_build_order_quantity %} +
    {% trans "Required for Sales Orders" %}{% decimal required_sales_order_quantity %} +
    {% trans "Allocated to Orders" %}{% decimal allocated %}

    +

    {% trans "Build Status" %}

    +
    {% trans "Can Build" %}{% decimal part.can_build %}
    {% trans "Building" %}{% decimal quantity_being_built %}
    +
    +
    +
    + +

    + + +

    + +
    +
    + + + {% if part.IPN %} + + + + + + {% endif %} + + + + + + + + + + + {% if part.revision %} + + + + + + {% endif %} {% if part.keywords %} - + {% endif %} {% if part.link %} - + {% endif %} @@ -154,95 +275,26 @@ - {% endif %} + {% endif %} + {% if part.default_location %} + + + + + + {% endif %} + {% if part.default_supplier %} + + + + + + {% endif %}
    {% trans "IPN" %}{{ part.IPN }}{% include "clip.html"%}
    {% trans "Name" %}{{ part.name }}{% include "clip.html"%}
    {% trans "Description" %}{{ part.description }}{% include "clip.html"%}
    {% trans "Revision" %}{{ part.revision }}{% include "clip.html"%}
    {% trans "Keywords" %}{{ part.keywords }}{{ part.keywords }}{% include "clip.html"%}
    {% trans "External Link" %}{{ part.link }}{{ part.link }}{% include "clip.html"%}
    {% trans "Latest Serial Number" %} {{ part.getLatestSerialNumber }}{% include "clip.html"%}
    {% trans "Default Location" %}{{ part.default_location }}
    {% trans "Default Supplier" %}{{ part.default_supplier }}
    -
    - -
    - {% if part.virtual %} -
    - {% trans "This is a virtual part" %} -
    - {% endif %} - {% if part.variant_of %} -
    - {% object_link 'part-detail' part.variant_of.id part.variant_of.full_name as link %} - {% blocktrans %}This part is a variant of {{link}}{% endblocktrans %} -
    - {% endif %} -
    -
    -
    - - - - - - - - - - - - - {% if on_order > 0 %} - - - - - - {% endif %} - {% if required_build_order_quantity > 0 %} - - - - - {% endif %} - {% if required_sales_order_quantity > 0 %} - - - - - {% endif %} - {% if allocated > 0 %} - - - - - - {% endif %} - - {% if not part.is_template %} - {% if part.assembly %} - - - - - - - - - - {% if quantity_being_built > 0 %} - - - - - - {% endif %} - {% endif %} - {% endif %} -
    -

    {% trans "Available Stock" %}

    -

    {% decimal available %}{% if part.units %} {{ part.units }}{% endif %}

    {% trans "In Stock" %}{% include "part/stock_count.html" %}
    {% trans "On Order" %}{% decimal on_order %}
    {% trans "Required for Build Orders" %}{% decimal required_build_order_quantity %} -
    {% trans "Required for Sales Orders" %}{% decimal required_sales_order_quantity %} -
    {% trans "Allocated to Orders" %}{% decimal allocated %}
    - {% trans "Build Status" %} -
    {% trans "Can Build" %}{% decimal part.can_build %}
    {% trans "Building" %}{% decimal quantity_being_built %}
    -
    +
    {% block page_content %} @@ -450,4 +502,42 @@ }); {% endif %} + $("#toggle-part-details").click(function() { + if (this.value == 'show') { + this.innerHTML = ' {% trans "Hide Part Details" %}'; + this.value = 'hide'; + // Store state of part details section + localStorage.setItem("part-details-show", true); + } else { + this.innerHTML = ' {% trans "Show Part Details" %}'; + this.value = 'show'; + // Store state of part details section + localStorage.setItem("part-details-show", false); + } + }); + + // Load part details section + window.onload = function() { + details_show = localStorage.getItem("part-details-show") + + if (details_show === 'true') { + console.log(details_show) + // Get collapsible details section + details = document.getElementById('collapsible-part-details'); + // Add "show" class + details.classList.add("in"); + // Get toggle + toggle = document.getElementById('toggle-part-details'); + // Change state of toggle + toggle.innerHTML = ' {% trans "Hide Part Details" %}'; + toggle.value = 'hide'; + } else { + // Get toggle + toggle = document.getElementById('toggle-part-details'); + // Change state of toggle + toggle.innerHTML = ' {% trans "Show Part Details" %}'; + toggle.value = 'show'; + } + } + {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 7581d659e1..e498bc09ba 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -161,7 +161,7 @@

    {% trans 'Stock Pricing' %} + The Supplier Unit Cost is the current purchase price for that supplier part.">

    {% if price_history|length > 0 %}
    diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index b23b71a2d6..7e739306b0 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -8,7 +8,6 @@ from __future__ import unicode_literals from django import forms from django.forms.utils import ErrorDict from django.utils.translation import ugettext_lazy as _ -from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError from mptt.fields import TreeNodeChoiceField @@ -241,14 +240,9 @@ class InstallStockForm(HelperForm): help_text=_('Stock item to install') ) - quantity_to_install = RoundingDecimalFormField( - max_digits=10, decimal_places=5, - initial=1, - label=_('Quantity'), - help_text=_('Stock quantity to assign'), - validators=[ - MinValueValidator(0.001) - ] + to_install = forms.BooleanField( + widget=forms.HiddenInput(), + required=False, ) notes = forms.CharField( @@ -261,7 +255,7 @@ class InstallStockForm(HelperForm): fields = [ 'part', 'stock_item', - 'quantity_to_install', + # 'quantity_to_install', 'notes', ] diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 8a00c1c5e6..d380ea3369 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -119,6 +119,11 @@

    {% trans "Installed Stock Items" %}

    +
    + +
    @@ -128,6 +133,20 @@ {% block js_ready %} {{ block.super }} + $('#stock-item-install').click(function() { + + launchModalForm( + "{% url 'stock-item-install' item.pk %}", + { + data: { + 'part': {{ item.part.pk }}, + 'install_item': true, + }, + reload: true, + } + ); + }); + loadInstalledInTable( $('#installed-table'), { diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 91c3209013..fa5e9da428 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -127,9 +127,11 @@
  • {% trans "Return to stock" %}
  • {% endif %} {% if item.belongs_to %} -
  • - {% trans "Uninstall" %} -
  • +
  • {% trans "Uninstall" %}
  • + {% else %} + {% if item.part.get_used_in %} +
  • {% trans "Install" %}
  • + {% endif %} {% endif %} @@ -461,13 +463,27 @@ $("#stock-serialize").click(function() { ); }); +$('#stock-install-in').click(function() { + + launchModalForm( + "{% url 'stock-item-install' item.pk %}", + { + data: { + 'part': {{ item.part.pk }}, + 'install_in': true, + }, + reload: true, + } + ); +}); + $('#stock-uninstall').click(function() { launchModalForm( "{% url 'stock-item-uninstall' %}", { data: { - 'items[]': [{{ item.pk}}], + 'items[]': [{{ item.pk }}], }, reload: true, } diff --git a/InvenTree/stock/templates/stock/item_install.html b/InvenTree/stock/templates/stock/item_install.html index 04798972d2..8a94f304d3 100644 --- a/InvenTree/stock/templates/stock/item_install.html +++ b/InvenTree/stock/templates/stock/item_install.html @@ -3,15 +3,31 @@ {% block pre_form_content %} +{% if install_item %}

    - {% trans "Install another StockItem into this item." %} + {% trans "Install another Stock Item into this item." %}

    {% trans "Stock items can only be installed if they meet the following criteria" %}:

    +{% elif install_in %} +

    + {% trans "Install this Stock Item in another stock item." %} +

    +

    + {% trans "Stock items can only be installed if they meet the following criteria" %}: + +

    +

    +{% endif %} + {% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 7b6fbc527e..2f602a93e1 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -536,36 +536,73 @@ class StockItemInstall(AjaxUpdateView): part = None + def get_params(self): + """ Retrieve GET parameters """ + + # Look at GET params + self.part_id = self.request.GET.get('part', None) + self.install_in = self.request.GET.get('install_in', False) + self.install_item = self.request.GET.get('install_item', False) + + if self.part_id is None: + # Look at POST params + self.part_id = self.request.POST.get('part', None) + + try: + self.part = Part.objects.get(pk=self.part_id) + except (ValueError, Part.DoesNotExist): + self.part = None + def get_stock_items(self): """ Return a list of stock items suitable for displaying to the user. Requirements: - Items must be in stock - - Filters: - - Items can be filtered by Part reference + - Items must be in BOM of stock item + - Items must be serialized """ - + + # Filter items in stock items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) - # Filter by Part association + # Filter serialized stock items + items = items.exclude(serial__isnull=True).exclude(serial__exact='') - # Look at GET params - part_id = self.request.GET.get('part', None) + if self.part: + # Filter for parts to install this item in + if self.install_in: + # Get parts using this part + allowed_parts = self.part.get_used_in() + # Filter + items = items.filter(part__in=allowed_parts) - if part_id is None: - # Look at POST params - part_id = self.request.POST.get('part', None) - - try: - self.part = Part.objects.get(pk=part_id) - items = items.filter(part=self.part) - except (ValueError, Part.DoesNotExist): - self.part = None + # Filter for parts to install in this item + if self.install_item: + # Get parts used in this part's BOM + bom_items = self.part.get_bom_items() + allowed_parts = [item.sub_part for item in bom_items] + # Filter + items = items.filter(part__in=allowed_parts) return items + def get_context_data(self, **kwargs): + """ Retrieve parameters and update context """ + + ctx = super().get_context_data(**kwargs) + + # Get request parameters + self.get_params() + + ctx.update({ + 'part': self.part, + 'install_in': self.install_in, + 'install_item': self.install_item, + }) + + return ctx + def get_initial(self): initials = super().get_initial() @@ -576,11 +613,16 @@ class StockItemInstall(AjaxUpdateView): if items.count() == 1: item = items.first() initials['stock_item'] = item.pk - initials['quantity_to_install'] = item.quantity if self.part: initials['part'] = self.part + try: + # Is this stock item being installed in the other stock item? + initials['to_install'] = self.install_in or not self.install_item + except AttributeError: + pass + return initials def get_form(self): @@ -593,6 +635,8 @@ class StockItemInstall(AjaxUpdateView): def post(self, request, *args, **kwargs): + self.get_params() + form = self.get_form() valid = form.is_valid() @@ -602,13 +646,19 @@ class StockItemInstall(AjaxUpdateView): data = form.cleaned_data other_stock_item = data['stock_item'] - quantity = data['quantity_to_install'] + # Quantity will always be 1 for serialized item + quantity = 1 notes = data['notes'] - # Install the other stock item into this one + # Get stock item this_stock_item = self.get_object() - this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes) + if data['to_install']: + # Install this stock item into the other stock item + other_stock_item.installStockItem(this_stock_item, quantity, request.user, notes) + else: + # Install the other stock item into this one + this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes) data = { 'form_valid': valid, diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 32166d972a..20829bad79 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -262,13 +262,13 @@ function loadBomTable(table, options) { cols.push( { field: 'price_range', - title: '{% trans "Buy Price" %}', + title: '{% trans "Supplier Cost" %}', sortable: true, formatter: function(value, row, index, field) { if (value) { return value; } else { - return "{% trans 'No pricing available' %}"; + return "{% trans 'No supplier pricing available' %}"; } } });