From 911b23ca249fee04c8360601a4cfe2708a8b30d1 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 5 Oct 2020 13:12:52 -0500 Subject: [PATCH 01/83] Added validation logic for user list to Group admin form --- InvenTree/users/admin.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 86d4bb1a86..df94ffab56 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -9,6 +9,8 @@ from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin +from django.core.exceptions import ValidationError +from django.utils.safestring import mark_safe from users.models import RuleSet @@ -62,6 +64,26 @@ class InvenTreeGroupAdminForm(forms.ModelForm): help_text=_('Select which users are assigned to this group') ) + def clean(self): + """ Validate that added Users don't belong to any group other than the current one + """ + + users = self.cleaned_data['users'].exclude(groups=self.instance) + + error_message = '' + for user in users: + if user.groups.all(): + error_message += f'
- {user.username} is in ' \ + f'"{Group.objects.get(user=user).name}" group' + + if error_message: + raise ValidationError( + mark_safe(_(f'The following users are already assigned to a group:' + f'{error_message}')) + ) + + return self.cleaned_data + def save_m2m(self): # Add the users to the Group. From 8499ced636dc2b6a01ba2efac9afbcf3555a3f6f Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 6 Oct 2020 10:38:06 -0500 Subject: [PATCH 02/83] Changed from validation error to warning message when group instance is saved --- InvenTree/users/admin.py | 56 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index f8454acb02..a116fdc94f 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ -from django.contrib import admin +from django.contrib import admin, messages from django import forms from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple @@ -64,26 +64,6 @@ class InvenTreeGroupAdminForm(forms.ModelForm): help_text=_('Select which users are assigned to this group') ) - def clean(self): - """ Validate that added Users don't belong to any group other than the current one - """ - - users = self.cleaned_data['users'].exclude(groups=self.instance) - - error_message = '' - for user in users: - if user.groups.all(): - error_message += f'
- {user.username} is in ' \ - f'"{Group.objects.get(user=user).name}" group' - - if error_message: - raise ValidationError( - mark_safe(_(f'The following users are already assigned to a group:' - f'{error_message}')) - ) - - return self.cleaned_data - def save_m2m(self): # Add the users to the Group. @@ -116,15 +96,37 @@ class RoleGroupAdmin(admin.ModelAdmin): filter_horizontal = ['permissions'] - # Save inlines before model - # https://stackoverflow.com/a/14860703/12794913 def save_model(self, request, obj, form, change): - pass # don't actually save the parent instance + """ + This method serves two purposes: + - show warning message whenever the group users belong to multiple groups + - skip saving of the group instance model as inlines needs to be saved before. + """ + + # Get form cleaned data + users = form.cleaned_data['users'] + + # Check for users who are members of multiple groups + warning_message = '' + for user in users: + if user.groups.all().count() > 1: + warning_message += f'
- {user.username} is member of: ' + for idx, group in enumerate(user.groups.all()): + warning_message += f'{group.name}' + if idx < len(user.groups.all()) - 1: + warning_message += ', ' + + # If any, display warning message when group is saved + if warning_message: + warning_message = mark_safe(_(f'The following users are members of multiple groups:' + f'{warning_message}')) + messages.add_message(request, messages.WARNING, warning_message) def save_formset(self, request, form, formset, change): - formset.save() # this will save the children - # update_fields is required to trigger permissions update - form.instance.save(update_fields=['name']) # form.instance is the parent + # Save inline Rulesets + formset.save() + # Save Group instance and update permissions + form.instance.save(update_fields=['name']) class InvenTreeUserAdmin(UserAdmin): From 01eee4d2089f8e01b220ec3965acd45257020269 Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 6 Oct 2020 10:40:58 -0500 Subject: [PATCH 03/83] Fixed style --- InvenTree/users/admin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index a116fdc94f..4614e5bc4c 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -9,7 +9,6 @@ from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin -from django.core.exceptions import ValidationError from django.utils.safestring import mark_safe from users.models import RuleSet @@ -97,7 +96,7 @@ class RoleGroupAdmin(admin.ModelAdmin): filter_horizontal = ['permissions'] def save_model(self, request, obj, form, change): - """ + """ This method serves two purposes: - show warning message whenever the group users belong to multiple groups - skip saving of the group instance model as inlines needs to be saved before. From 3d9223c2eedfbf914a65f3d2b074d84647c14b5f Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 15 Oct 2020 14:11:24 -0500 Subject: [PATCH 04/83] Introduced PartRelated model to store part relationships --- InvenTree/part/migrations/0052_partrelated.py | 22 ++++++++ InvenTree/part/models.py | 51 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 InvenTree/part/migrations/0052_partrelated.py diff --git a/InvenTree/part/migrations/0052_partrelated.py b/InvenTree/part/migrations/0052_partrelated.py new file mode 100644 index 0000000000..c84566b3ea --- /dev/null +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.7 on 2020-10-15 18:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0051_bomitem_optional'), + ] + + operations = [ + migrations.CreateModel( + name='PartRelated', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('part_1', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_1', to='part.Part')), + ('part_2', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', to='part.Part')), + ], + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d427409c71..26ea555ed9 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1254,6 +1254,25 @@ class Part(MPTTModel): return self.get_descendants(include_self=False) + def fetch_related_parts(self): + """ Return all related parts """ + + related_parts = [] + + parts_1 = self.related_parts_1.filter(part_1__id=self.pk) + + parts_2 = self.related_parts_2.filter(part_2__id=self.pk) + + for part in parts_1: + # Append + related_parts.append(part.part_2) + + for part in parts_2: + # Append + related_parts.append(part.part_1) + + return related_parts + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment @@ -1723,3 +1742,35 @@ class BomItem(models.Model): pmax = decimal2string(pmax) return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax) + + +class PartRelated(models.Model): + """ Store and handle related parts (eg. mating connector, crimps, etc.) """ + + part_1 = models.ForeignKey(Part, related_name='related_parts_1', on_delete=models.DO_NOTHING) + + part_2 = models.ForeignKey(Part, related_name='related_parts_2', on_delete=models.DO_NOTHING) + + def create_relationship(self, part_1, part_2): + + parts = Part.objects.all() + + # Check if part exist + if (part_1 in parts and part_2 in parts) and (part_1 is not part_2): + # Add relationship + self.part_1 = part_1 + self.part_2 = part_2 + self.save() + + return True + else: + return False + + @classmethod + def create(cls, part_1, part_2): + related_part = cls() + related_part.create_relationship(part_1, part_2) + return related_part + + def __str__(self): + return f'{part_1} <-> {part_2}' From 8579abb9c2718ef6f25d3e6dfabf0537912f996d Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 15 Oct 2020 16:58:39 -0500 Subject: [PATCH 05/83] Added related parts urls, views, form and templates Adding related part relationships work but are still not shown in the part detail page --- InvenTree/part/forms.py | 21 ++++++- InvenTree/part/models.py | 33 +++++++---- InvenTree/part/templates/part/related.html | 46 +++++++++++++++ InvenTree/part/templates/part/tabs.html | 3 + InvenTree/part/urls.py | 9 +++ InvenTree/part/views.py | 69 +++++++++++++++++++++- InvenTree/templates/js/part.html | 35 +++++++++++ 7 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 InvenTree/part/templates/part/related.html diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index c64bbb8362..c463fda891 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -13,7 +13,7 @@ from mptt.fields import TreeNodeChoiceField from django import forms from django.utils.translation import ugettext as _ -from .models import Part, PartCategory, PartAttachment +from .models import Part, PartCategory, PartAttachment, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartTestTemplate @@ -104,6 +104,25 @@ class BomUploadSelectFile(HelperForm): ] +class CreatePartRelatedForm(HelperForm): + """ Form for creating a PartRelated object """ + + class Meta: + model = PartRelated + fields = [ + 'part_1', + 'part_2', + ] + labels = { + 'part_2': _('Related Part'), + } + + def save(self): + """ Disable model saving """ + + return super(CreatePartRelatedForm, self).save(commit=False) + + class EditPartAttachmentForm(HelperForm): """ Form for editing a PartAttachment object """ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 26ea555ed9..aeef72da70 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1264,11 +1264,11 @@ class Part(MPTTModel): parts_2 = self.related_parts_2.filter(part_2__id=self.pk) for part in parts_1: - # Append + # Append related_parts.append(part.part_2) for part in parts_2: - # Append + # Append related_parts.append(part.part_1) return related_parts @@ -1749,28 +1749,41 @@ class PartRelated(models.Model): part_1 = models.ForeignKey(Part, related_name='related_parts_1', on_delete=models.DO_NOTHING) - part_2 = models.ForeignKey(Part, related_name='related_parts_2', on_delete=models.DO_NOTHING) + part_2 = models.ForeignKey(Part, related_name='related_parts_2', on_delete=models.DO_NOTHING, + help_text=_('Choose Related Part')) + + def __str__(self): + return f'{self.part_1} <-> {self.part_2}' def create_relationship(self, part_1, part_2): + ''' Create relationship between two parts ''' + + validate = True parts = Part.objects.all() + related_parts = PartRelated.objects.all() - # Check if part exist + # Check if part exist and there are not the same part if (part_1 in parts and part_2 in parts) and (part_1 is not part_2): + # Check if relation exists already + for relation in related_parts: + if (part_1 == relation.part_1 and part_2 == relation.part_2) \ + or (part_1 == relation.part_2 and part_2 == relation.part_1): + validate = False + else: + validate = False + + if validate: # Add relationship self.part_1 = part_1 self.part_2 = part_2 self.save() - return True - else: - return False + return validate @classmethod def create(cls, part_1, part_2): + ''' Create PartRelated object ''' related_part = cls() related_part.create_relationship(part_1, part_2) return related_part - - def __str__(self): - return f'{part_1} <-> {part_2}' diff --git a/InvenTree/part/templates/part/related.html b/InvenTree/part/templates/part/related.html new file mode 100644 index 0000000000..f9175ed590 --- /dev/null +++ b/InvenTree/part/templates/part/related.html @@ -0,0 +1,46 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include 'part/tabs.html' with tab='related-parts' %} + +

{% trans "Related Parts" %}

+
+ +
+
+ + +
+
+ + + + + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +loadPartRelatedTable($("#table-related-part"), { + url: "{% url 'api-part-list' %}", + params: { + part: {{ part.id }}, + }, +}); + +$("#add-related-part").click(function() { + launchModalForm("{% url 'part-related-create' %}", { + data: { + part: {{ part.id }}, + }, + reload: true, + }); +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 8322a225bc..c96de9b825 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -63,6 +63,9 @@ {% endif %} + + {% trans "Related Parts" %} {% if part.related_count > 0 %}{{ part.related_count }}{% endif %} + {% trans "Attachments" %} {% if part.attachment_count > 0 %}{{ part.attachment_count }}{% endif %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 32cd1b0615..e858e0583c 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -12,6 +12,11 @@ from django.conf.urls import url, include from . import views +part_related_urls = [ + url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'), + url(r'^(?P\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'), +] + part_attachment_urls = [ url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), url(r'^(?P\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), @@ -60,6 +65,7 @@ part_detail_urls = [ url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'), url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), + url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), @@ -112,6 +118,9 @@ part_urls = [ # Part category url(r'^category/(?P\d+)/', include(part_category_urls)), + # Part related + url(r'^related-parts/', include(part_related_urls)), + # Part attachments url(r'^attachment/', include(part_attachment_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6498774285..f49d57bbe3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -21,7 +21,7 @@ import os from rapidfuzz import fuzz from decimal import Decimal, InvalidOperation -from .models import PartCategory, Part, PartAttachment +from .models import PartCategory, Part, PartAttachment, PartRelated from .models import PartParameterTemplate, PartParameter from .models import BomItem from .models import match_part_names @@ -70,6 +70,73 @@ class PartIndex(InvenTreeRoleMixin, ListView): return context +class PartRelatedCreate(AjaxCreateView): + """ View for creating a new PartRelated object + + - The view only makes sense if a Part object is passed to it + """ + model = PartRelated + form_class = part_forms.CreatePartRelatedForm + ajax_form_title = _("Add Related Part") + ajax_template_name = "modal_form.html" + role_required = 'part.change' + + # TODO: QuerySet should not show parts already related to object + + def get_initial(self): + """ Point part_1 to parent part """ + + initials = {} + + part_id = self.request.GET.get('part', None) + + if part_id: + try: + initials['part_1'] = Part.objects.get(pk=part_id) + except (Part.DoesNotExist, ValueError): + pass + + return initials + + def get_form(self): + """ Create a form to upload a new PartRelated + + - Hide the 'part_1' field (parent part) + """ + + form = super(AjaxCreateView, self).get_form() + + form.fields['part_1'].widget = HiddenInput() + + return form + + def post_save(self): + """ Save PartRelated model (POST method does not) """ + + form = self.get_form() + + if form.is_valid(): + print('form is valid!') + + part_1 = form.cleaned_data['part_1'] + part_2 = form.cleaned_data['part_2'] + + print(f'{part_1=}') + print(f'{part_2=}') + + PartRelated.create(part_1, part_2) + + +class PartRelatedDelete(AjaxDeleteView): + """ View for deleting a PartRelated object """ + + model = PartRelated + ajax_form_title = _("Delete Related Part") + ajax_template_name = "related_delete.html" + context_object_name = "related" + role_required = 'part.change' + + class PartAttachmentCreate(AjaxCreateView): """ View for creating a new PartAttachment object diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index e5fafef070..fbe1c945e0 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -163,6 +163,41 @@ function loadSimplePartTable(table, url, options={}) { } +function loadPartRelatedTable(table, options={}) { + /* Load related parts table */ + + var columns = [ + { + field: options.params['part'], + title: '{% trans 'Part' %}', + sortable: true, + sortName: 'name', + formatter: function(value, row, index, field) { + + var name = ''; + + if (row.IPN) { + name += row.IPN + ' | ' + row.name; + } else { + name += row.name; + } + + return renderLink(name, '/part/' + row.pk + '/'); + } + } + ]; + + $(table).inventreeTable({ + sortName: 'part', + groupBy: false, + name: options.name || 'related_parts', + formatNoMatches: function() { return "{% trans "No parts found" %}"; }, + columns: columns, + showColumns: true, + }); +} + + function loadParametricPartTable(table, options={}) { /* Load parametric table for part parameters * From 34e4409e7f0521313ce3f5b29a47dbda27ca0edd Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 16 Oct 2020 13:50:31 -0500 Subject: [PATCH 06/83] Functional checkpoint: add/delete related parts from template --- InvenTree/part/models.py | 30 +++++++---- InvenTree/part/templates/part/related.html | 59 ++++++++++++++++------ InvenTree/part/templates/part/tabs.html | 2 +- InvenTree/part/views.py | 28 ++++++---- InvenTree/templates/js/part.html | 35 ------------- 5 files changed, 83 insertions(+), 71 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index aeef72da70..bf979bfbb5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1254,25 +1254,32 @@ class Part(MPTTModel): return self.get_descendants(include_self=False) - def fetch_related_parts(self): - """ Return all related parts """ + def get_related_parts(self): + """ Return list of tuples for all related parts: + - first value is PartRelated object + - second value is matching Part object + """ related_parts = [] - parts_1 = self.related_parts_1.filter(part_1__id=self.pk) + related_parts_1 = self.related_parts_1.filter(part_1__id=self.pk) - parts_2 = self.related_parts_2.filter(part_2__id=self.pk) + related_parts_2 = self.related_parts_2.filter(part_2__id=self.pk) - for part in parts_1: - # Append - related_parts.append(part.part_2) + for related_part in related_parts_1: + # Add to related parts list + related_parts.append((related_part, related_part.part_2)) - for part in parts_2: - # Append - related_parts.append(part.part_1) + for related_part in related_parts_2: + # Add to related parts list + related_parts.append((related_part, related_part.part_1)) return related_parts + @property + def related_count(self): + return len(self.get_related_parts()) + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment @@ -1783,7 +1790,8 @@ class PartRelated(models.Model): @classmethod def create(cls, part_1, part_2): - ''' Create PartRelated object ''' + ''' Create PartRelated object and relationship between two parts ''' + related_part = cls() related_part.create_relationship(part_1, part_2) return related_part diff --git a/InvenTree/part/templates/part/related.html b/InvenTree/part/templates/part/related.html index f9175ed590..8fd167b592 100644 --- a/InvenTree/part/templates/part/related.html +++ b/InvenTree/part/templates/part/related.html @@ -18,7 +18,32 @@ - + + + + + + + + {% for item in part.get_related_parts %} + {% with part_related=item.0 part=item.1 %} + + + + {% endwith %} + {% endfor %} + @@ -27,20 +52,24 @@ {% block js_ready %} {{ block.super }} -loadPartRelatedTable($("#table-related-part"), { - url: "{% url 'api-part-list' %}", - params: { - part: {{ part.id }}, - }, -}); - -$("#add-related-part").click(function() { - launchModalForm("{% url 'part-related-create' %}", { - data: { - part: {{ part.id }}, - }, - reload: true, + $('#table-related-part').inventreeTable({ + }); + + $("#add-related-part").click(function() { + launchModalForm("{% url 'part-related-create' %}", { + data: { + part: {{ part.id }}, + }, + reload: true, + }); + }); + + $('.delete-related-part').click(function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + reload: true, + }); }); -}); {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index c96de9b825..8bfaba4d89 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -64,7 +64,7 @@ {% endif %} - {% trans "Related Parts" %} {% if part.related_count > 0 %}{{ part.related_count }}{% endif %} + {% trans "Related" %} {% if part.related_count > 0 %}{{ part.related_count }}{% endif %} {% trans "Attachments" %} {% if part.attachment_count > 0 %}{{ part.attachment_count }}{% endif %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index f49d57bbe3..6ae782ab2c 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -81,10 +81,8 @@ class PartRelatedCreate(AjaxCreateView): ajax_template_name = "modal_form.html" role_required = 'part.change' - # TODO: QuerySet should not show parts already related to object - def get_initial(self): - """ Point part_1 to parent part """ + """ Set parent part as part_1 field """ initials = {} @@ -102,12 +100,30 @@ class PartRelatedCreate(AjaxCreateView): """ Create a form to upload a new PartRelated - Hide the 'part_1' field (parent part) + - Display parts which are not yet related """ form = super(AjaxCreateView, self).get_form() form.fields['part_1'].widget = HiddenInput() + try: + # Get parent part + parent_part = self.get_initial()['part_1'] + # Get existing related parts + related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()] + + # Build updated choice list excluding parts already related to parent part + updated_choices = [] + for choice in form.fields["part_2"].choices: + if choice[0] not in related_parts: + updated_choices.append(choice) + + # Update choices for related part + form.fields['part_2'].choices = updated_choices + except KeyError: + pass + return form def post_save(self): @@ -116,14 +132,9 @@ class PartRelatedCreate(AjaxCreateView): form = self.get_form() if form.is_valid(): - print('form is valid!') - part_1 = form.cleaned_data['part_1'] part_2 = form.cleaned_data['part_2'] - print(f'{part_1=}') - print(f'{part_2=}') - PartRelated.create(part_1, part_2) @@ -132,7 +143,6 @@ class PartRelatedDelete(AjaxDeleteView): model = PartRelated ajax_form_title = _("Delete Related Part") - ajax_template_name = "related_delete.html" context_object_name = "related" role_required = 'part.change' diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index fbe1c945e0..e5fafef070 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -163,41 +163,6 @@ function loadSimplePartTable(table, url, options={}) { } -function loadPartRelatedTable(table, options={}) { - /* Load related parts table */ - - var columns = [ - { - field: options.params['part'], - title: '{% trans 'Part' %}', - sortable: true, - sortName: 'name', - formatter: function(value, row, index, field) { - - var name = ''; - - if (row.IPN) { - name += row.IPN + ' | ' + row.name; - } else { - name += row.name; - } - - return renderLink(name, '/part/' + row.pk + '/'); - } - } - ]; - - $(table).inventreeTable({ - sortName: 'part', - groupBy: false, - name: options.name || 'related_parts', - formatNoMatches: function() { return "{% trans "No parts found" %}"; }, - columns: columns, - showColumns: true, - }); -} - - function loadParametricPartTable(table, options={}) { /* Load parametric table for part parameters * From 0b26d68d0feca4368aebd4699c367ac8e6947461 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 16 Oct 2020 15:29:58 -0500 Subject: [PATCH 07/83] Added admin view, improved validation of part related relationship --- InvenTree/part/admin.py | 8 ++++- InvenTree/part/models.py | 36 +++++++++++++++++----- InvenTree/part/templates/part/related.html | 2 ++ InvenTree/part/test_views.py | 23 ++++++++++++++ InvenTree/part/views.py | 6 ++-- InvenTree/users/models.py | 1 + 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index f0c9e3f233..7476197547 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -9,7 +9,7 @@ from import_export.fields import Field import import_export.widgets as widgets from .models import PartCategory, Part -from .models import PartAttachment, PartStar +from .models import PartAttachment, PartStar, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartTestTemplate @@ -121,6 +121,11 @@ class PartCategoryAdmin(ImportExportModelAdmin): search_fields = ('name', 'description') +class PartRelatedAdmin(admin.ModelAdmin): + ''' Class to manage PartRelated objects ''' + pass + + class PartAttachmentAdmin(admin.ModelAdmin): list_display = ('part', 'attachment', 'comment') @@ -279,6 +284,7 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin): admin.site.register(Part, PartAdmin) admin.site.register(PartCategory, PartCategoryAdmin) +admin.site.register(PartRelated, PartRelatedAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartStar, PartStarAdmin) admin.site.register(BomItem, BomItemAdmin) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index bf979bfbb5..db0be1952f 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1754,16 +1754,18 @@ class BomItem(models.Model): class PartRelated(models.Model): """ Store and handle related parts (eg. mating connector, crimps, etc.) """ - part_1 = models.ForeignKey(Part, related_name='related_parts_1', on_delete=models.DO_NOTHING) + part_1 = models.ForeignKey(Part, related_name='related_parts_1', + on_delete=models.DO_NOTHING) - part_2 = models.ForeignKey(Part, related_name='related_parts_2', on_delete=models.DO_NOTHING, - help_text=_('Choose Related Part')) + part_2 = models.ForeignKey(Part, related_name='related_parts_2', + on_delete=models.DO_NOTHING, + help_text=_('Select Related Part')) def __str__(self): - return f'{self.part_1} <-> {self.part_2}' + return f'{self.part_1} <--> {self.part_2}' - def create_relationship(self, part_1, part_2): - ''' Create relationship between two parts ''' + def validate(self, part_1, part_2): + ''' Validate that the two parts relationship is unique ''' validate = True @@ -1771,15 +1773,35 @@ class PartRelated(models.Model): related_parts = PartRelated.objects.all() # Check if part exist and there are not the same part - if (part_1 in parts and part_2 in parts) and (part_1 is not part_2): + if (part_1 in parts and part_2 in parts) and (part_1.pk != part_2.pk): # Check if relation exists already for relation in related_parts: if (part_1 == relation.part_1 and part_2 == relation.part_2) \ or (part_1 == relation.part_2 and part_2 == relation.part_1): validate = False + break else: validate = False + return validate + + def clean(self): + ''' Overwrite clean method to check that relation is unique ''' + + validate = self.validate(self.part_1, self.part_2) + + if not validate: + error_message = _('Error creating relationship: check that ' + 'the part is not related to itself ' + 'and that the relationship is unique') + + raise ValidationError(error_message) + + def create_relationship(self, part_1, part_2): + ''' Create relationship between two parts ''' + + validate = self.validate(part_1, part_2) + if validate: # Add relationship self.part_1 = part_1 diff --git a/InvenTree/part/templates/part/related.html b/InvenTree/part/templates/part/related.html index 8fd167b592..8c5cc074c8 100644 --- a/InvenTree/part/templates/part/related.html +++ b/InvenTree/part/templates/part/related.html @@ -11,10 +11,12 @@
+ {% if roles.part.change %} + {% endif %}
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index d8c345d243..1ad5c33e45 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -201,6 +201,29 @@ class PartTests(PartViewTestCase): self.assertEqual(response.status_code, 200) +class PartRelatedTests(PartViewTestCase): + + def test_valid_create(self): + """ test creation of an attachment for a valid part """ + + response = self.client.get(reverse('part-related-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # TODO - Create a new attachment using this view + + def test_invalid_create(self): + """ test creation of an attachment for an invalid part """ + + # TODO + pass + + def test_edit(self): + """ test editing an attachment """ + + # TODO + pass + + class PartAttachmentTests(PartViewTestCase): def test_valid_create(self): diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6ae782ab2c..b4d8839e13 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -113,10 +113,12 @@ class PartRelatedCreate(AjaxCreateView): # Get existing related parts related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()] - # Build updated choice list excluding parts already related to parent part + # Build updated choice list excluding + # - parts already related to parent part + # - the parent part itself updated_choices = [] for choice in form.fields["part_2"].choices: - if choice[0] not in related_parts: + if (choice[0] not in related_parts) and (choice[0] != parent_part.pk): updated_choices.append(choice) # Update choices for related part diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index d3c713d07d..d1a3a3b084 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -57,6 +57,7 @@ class RuleSet(models.Model): 'part_parttesttemplate', 'part_partparametertemplate', 'part_partparameter', + 'part_partrelated', ], 'stock': [ 'stock_stockitem', From 5a6cac43f5d734d5948bc6b2069541d03d0fd156 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 16 Oct 2020 15:42:23 -0500 Subject: [PATCH 08/83] Updated migration --- InvenTree/part/migrations/0052_partrelated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/migrations/0052_partrelated.py b/InvenTree/part/migrations/0052_partrelated.py index c84566b3ea..a8672ba7dc 100644 --- a/InvenTree/part/migrations/0052_partrelated.py +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.7 on 2020-10-15 18:51 +# Generated by Django 3.0.7 on 2020-10-16 20:42 from django.db import migrations, models import django.db.models.deletion @@ -16,7 +16,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('part_1', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_1', to='part.Part')), - ('part_2', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', to='part.Part')), + ('part_2', models.ForeignKey(help_text='Select Related Part', on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', to='part.Part')), ], ), ] From fdcef7b69980d6ff8fa8e207703473900821515a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 20:37:57 +1100 Subject: [PATCH 09/83] Add "install_into" field for BuildItem - Points to which output stock item it will be going into --- .../migrations/0021_auto_20201020_0908.py | 25 +++++++++++++++++++ InvenTree/build/models.py | 13 +++++++++- InvenTree/stock/api.py | 6 +++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 InvenTree/build/migrations/0021_auto_20201020_0908.py diff --git a/InvenTree/build/migrations/0021_auto_20201020_0908.py b/InvenTree/build/migrations/0021_auto_20201020_0908.py new file mode 100644 index 0000000000..5fa450f5c7 --- /dev/null +++ b/InvenTree/build/migrations/0021_auto_20201020_0908.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.7 on 2020-10-20 09:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0052_stockitem_is_building'), + ('build', '0020_auto_20201019_1325'), + ] + + operations = [ + migrations.AddField( + model_name='builditem', + name='install_into', + field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem'), + ), + migrations.AlterField( + model_name='builditem', + name='stock_item', + field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 877affd144..3afc7d5d10 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -568,7 +568,7 @@ class BuildItem(models.Model): 'stock.StockItem', on_delete=models.CASCADE, related_name='allocations', - help_text=_('Stock Item to allocate to build'), + help_text=_('Source stock item'), limit_choices_to={ 'build_order': None, 'sales_order': None, @@ -583,3 +583,14 @@ class BuildItem(models.Model): validators=[MinValueValidator(0)], help_text=_('Stock quantity to allocate to build') ) + + install_into = models.ForeignKey( + 'stock.StockItem', + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='items_to_install', + help_text=_('Destination stock item'), + limit_choices_to={ + 'is_building': True, + } + ) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index dabeaa20ea..5d71d00a0c 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -493,6 +493,12 @@ class StockList(generics.ListCreateAPIView): if build_order: queryset = queryset.filter(build_order=build_order) + is_building = params.get('is_building', None) + + if is_building: + is_building = str2bool(is_building) + queryset = queryset.filter(is_building=is_building) + sales_order = params.get('sales_order', None) if sales_order: From 28460b30238512eafd9bdc6b4fb6c4b0601b1f73 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 20:42:29 +1100 Subject: [PATCH 10/83] Validate that the BuildItem quantity is an integer --- InvenTree/build/models.py | 17 ++++++++--------- InvenTree/stock/serializers.py | 1 + 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 3afc7d5d10..f4a66e6b6f 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -32,7 +32,7 @@ from part import models as PartModels class Build(MPTTModel): - """ A Build object organises the creation of new parts from the component parts. + """ A Build object organises the creation of new StockItem objects from other existing StockItem objects. Attributes: part: The part to be built (from component BOM items) @@ -70,15 +70,14 @@ class Build(MPTTModel): super().clean() - try: - if self.part.trackable: - if not self.quantity == int(self.quantity): - raise ValidationError({ - 'quantity': _("Build quantity must be integer value for trackable parts") - }) - except PartModels.Part.DoesNotExist: - pass + # Build quantity must be an integer + # Maybe in the future this will be adjusted? + if not self.quantity == int(self.quantity): + raise ValidationError({ + 'quantity': _("Build quantity must be integer value for trackable parts") + }) + reference = models.CharField( unique=True, max_length=64, diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 4a9b5a886b..d257f12f97 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -157,6 +157,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'customer', 'build_order', 'in_stock', + 'is_building', 'link', 'location', 'location_detail', From ac79e131bc7685643818e98b9346e29364b5bb75 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 21:01:51 +1100 Subject: [PATCH 11/83] Add "destination" field to BuildOrder --- InvenTree/InvenTree/views.py | 4 +-- InvenTree/build/forms.py | 5 ++-- .../migrations/0022_auto_20201020_0953.py | 26 +++++++++++++++++++ InvenTree/build/models.py | 11 +++++++- InvenTree/build/templates/build/index.html | 2 +- InvenTree/build/views.py | 13 ++++++++-- InvenTree/part/templates/part/detail.html | 2 +- 7 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 InvenTree/build/migrations/0022_auto_20201020_0953.py diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index bb7c1e6f5d..eb168ad547 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -322,7 +322,7 @@ class AjaxCreateView(AjaxMixin, CreateView): """ pass - def post_save(self, **kwargs): + def post_save(self, new_object, **kwargs): """ Hook for doing something with the created object after it is saved """ @@ -356,7 +356,7 @@ class AjaxCreateView(AjaxMixin, CreateView): self.pre_save() self.object = self.form.save() - self.post_save() + self.post_save(self.object) # Return the PK of the newly-created object data['pk'] = self.object.pk diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 74227adb9c..e39d0ba9cf 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -35,10 +35,11 @@ class EditBuildForm(HelperForm): 'title', 'part', 'quantity', + 'batch', + 'take_from', + 'destination', 'parent', 'sales_order', - 'take_from', - 'batch', 'link', ] diff --git a/InvenTree/build/migrations/0022_auto_20201020_0953.py b/InvenTree/build/migrations/0022_auto_20201020_0953.py new file mode 100644 index 0000000000..62a82ce7fd --- /dev/null +++ b/InvenTree/build/migrations/0022_auto_20201020_0953.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.7 on 2020-10-20 09:53 + +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0052_stockitem_is_building'), + ('build', '0021_auto_20201020_0908'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='destination', + field=models.ForeignKey(blank=True, help_text='Select location where the completed items will be stored', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_builds', to='stock.StockLocation', verbose_name='Destination Location'), + ), + migrations.AlterField( + model_name='build', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, help_text='BuildOrder to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f4a66e6b6f..ca4bb586fe 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -102,7 +102,7 @@ class Build(MPTTModel): blank=True, null=True, related_name='children', verbose_name=_('Parent Build'), - help_text=_('Parent build to which this build is allocated'), + help_text=_('BuildOrder to which this build is allocated'), ) part = models.ForeignKey( @@ -137,6 +137,15 @@ class Build(MPTTModel): help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)') ) + destination = models.ForeignKey( + 'stock.StockLocation', + verbose_name=_('Destination Location'), + on_delete=models.SET_NULL, + related_name='incoming_builds', + null=True, blank=True, + help_text=_('Select location where the completed items will be stored'), + ) + quantity = models.PositiveIntegerField( verbose_name=_('Build Quantity'), default=1, diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index d72807f9f1..c15b2c2d33 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -43,7 +43,7 @@ InvenTree | {% trans "Build Orders" %} launchModalForm( "{% url 'build-create' %}", { - follow: true + follow: true, } ); }); diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 1296e42fae..c1431b5730 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -412,8 +412,17 @@ class BuildCreate(AjaxCreateView): initials = super(BuildCreate, self).get_initial().copy() - # User has provided a Part ID - initials['part'] = self.request.GET.get('part', None) + part = self.request.GET.get('part', None) + + if part: + + try: + part = Part.objects.get(pk=part) + # User has provided a Part ID + initials['part'] = part + initials['destination'] = part.get_default_location() + except (ValueError, Part.DoesNotExist): + pass initials['reference'] = Build.getNextBuildNumber() diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 2714347414..facb8003a6 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -41,7 +41,7 @@ {% if part.getLatestSerialNumber %} {{ part.getLatestSerialNumber }} {% else %} - {% trans "No serial numbers recorded" %} + {% trans "No serial numbers recorded" %} {% endif %} From 2df0f03a9a0ef58037b96fac210d8ba0b30a2196 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 21:10:36 +1100 Subject: [PATCH 12/83] Change "ALLOCATED" to "PRODUCTION" --- InvenTree/InvenTree/status_codes.py | 8 ++++---- .../migrations/0023_auto_20201020_1009.py | 19 +++++++++++++++++++ InvenTree/build/views.py | 9 +++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 InvenTree/build/migrations/0023_auto_20201020_1009.py diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 2032ec75d8..b527009ede 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -214,25 +214,25 @@ class BuildStatus(StatusCode): # Build status codes PENDING = 10 # Build is pending / active - ALLOCATED = 20 # Parts have been removed from stock + PRODUCTION = 20 # BuildOrder is in production CANCELLED = 30 # Build was cancelled COMPLETE = 40 # Build is complete options = { PENDING: _("Pending"), - ALLOCATED: _("Allocated"), + PRODUCTION: _("Production"), CANCELLED: _("Cancelled"), COMPLETE: _("Complete"), } colors = { PENDING: 'blue', - ALLOCATED: 'blue', + PRODUCTION: 'blue', COMPLETE: 'green', CANCELLED: 'red', } ACTIVE_CODES = [ PENDING, - ALLOCATED + PRODUCTION, ] diff --git a/InvenTree/build/migrations/0023_auto_20201020_1009.py b/InvenTree/build/migrations/0023_auto_20201020_1009.py new file mode 100644 index 0000000000..be5652d043 --- /dev/null +++ b/InvenTree/build/migrations/0023_auto_20201020_1009.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-10-20 10:09 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0022_auto_20201020_0953'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Production'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'), + ), + ] diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index c1431b5730..02e4a8d735 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -440,6 +440,15 @@ class BuildCreate(AjaxCreateView): 'success': _('Created new build'), } + def post_save(self, new_object): + """ + Called immediately after the build has been created. + """ + + build = new_object + + print("Created:", build) + class BuildUpdate(AjaxUpdateView): """ View for editing a Build object """ From 2e4613e702b776209c559bc3474be3c00044743e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 22:37:21 +1100 Subject: [PATCH 13/83] Updates to build forms / etc --- .../static/script/inventree/modals.js | 26 ++++++++++++++++ InvenTree/build/templates/build/index.html | 7 +---- InvenTree/part/templates/part/build.html | 13 +++----- InvenTree/templates/js/build.html | 31 +++++++++++++++++++ 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/InvenTree/static/script/inventree/modals.js index 49de96468f..d7d42ac941 100644 --- a/InvenTree/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/InvenTree/static/script/inventree/modals.js @@ -134,6 +134,32 @@ function reloadFieldOptions(fieldName, options) { } +function enableField(fieldName, enabled, options={}) { + /* Enable (or disable) a particular field in a modal. + * + * Args: + * - fieldName: The name of the field + * - enabled: boolean enabled / disabled status + * - options: + */ + + var modal = options.modal || '#modal-form'; + + var field = getFieldByName(modal, fieldName); + + field.prop("disabled", !enabled); +} + +function clearField(fieldName, options={}) { + + var modal = options.modal || '#modal-form'; + + var field = getFieldByName(modal, fieldName); + + field.val(""); +} + + function partialMatcher(params, data) { /* Replacement function for the 'matcher' parameter for a select2 dropdown. diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index c15b2c2d33..44fc8cfecd 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -40,12 +40,7 @@ InvenTree | {% trans "Build Orders" %} $("#collapse-item-active").collapse().show(); $("#new-build").click(function() { - launchModalForm( - "{% url 'build-create' %}", - { - follow: true, - } - ); + newBuildOrder(); }); loadBuildTable($("#build-table"), { diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index bfd72a2f70..d7ca33c673 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -29,14 +29,11 @@ {% block js_ready %} {{ block.super }} $("#start-build").click(function() { - launchModalForm( - "{% url 'build-create' %}", - { - follow: true, - data: { - part: {{ part.id }} - } - }); + newBuildOrder({ + data: { + part: {{ part.id }}, + } + }); }); loadBuildTable($("#build-table"), { diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 6a12b97bfd..b72d3d179a 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -1,6 +1,37 @@ {% load i18n %} {% load inventree_extras %} +function newBuildOrder(options={}) { + /* Launch modal form to create a new BuildOrder. + */ + + launchModalForm( + "{% url 'build-create' %}", + { + follow: true, + data: options.data || {}, + callback: [ + { + field: 'part', + action: function(value) { + inventreeGet( + `/api/part/${value}/`, {}, + { + success: function(response) { + + //enableField('serial_numbers', response.trackable); + //clearField('serial_numbers'); + } + } + ); + }, + } + ], + } + ) + +} + function loadBuildTable(table, options) { // Display a table of Build objects From 652c2dbcbecb99e2c1041884a6a9964d564f27c5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 22:37:55 +1100 Subject: [PATCH 14/83] Automagically disable 'serial_numbers' field for StockItemCreate form - Yay, ajax magic! --- InvenTree/stock/forms.py | 2 +- InvenTree/stock/views.py | 9 ++++----- InvenTree/templates/js/stock.html | 13 +++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 548a03ae90..d9937f3106 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -108,7 +108,7 @@ class ConvertStockItemForm(HelperForm): class CreateStockItemForm(HelperForm): """ Form for creating a new StockItem """ - serial_numbers = forms.CharField(label='Serial numbers', required=False, help_text=_('Enter unique serial numbers (or leave blank)')) + serial_numbers = forms.CharField(label=_('Serial numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)')) def __init__(self, *args, **kwargs): diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 65c8f30893..a60ce6b55c 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1510,11 +1510,8 @@ class StockItemCreate(AjaxCreateView): # form.fields['part'].widget = HiddenInput() # Trackable parts get special consideration: - if part.trackable: - form.fields['delete_on_deplete'].widget = HiddenInput() - form.fields['delete_on_deplete'].initial = False - else: - form.fields['serial_numbers'].widget = HiddenInput() + form.fields['delete_on_deplete'].disabled = not part.trackable + form.fields['serial_numbers'].disabled = not part.trackable # If the part is NOT purchaseable, hide the supplier_part field if not part.purchaseable: @@ -1539,6 +1536,8 @@ class StockItemCreate(AjaxCreateView): # We must not provide *any* options for SupplierPart form.fields['supplier_part'].queryset = SupplierPart.objects.none() + form.fields['serial_numbers'].disabled = True + # Otherwise if the user has selected a SupplierPart, we know what Part they meant! if form['supplier_part'].value() is not None: pass diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index c4209733ef..150765a305 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -769,6 +769,7 @@ function createNewStockItem(options) { field: 'part', action: function(value) { + // Reload options for supplier part reloadFieldOptions( 'supplier_part', { @@ -782,6 +783,18 @@ function createNewStockItem(options) { } } ); + + // Disable serial number field if the part is not trackable + inventreeGet( + `/api/part/${value}/`, {}, + { + success: function(response) { + + enableField('serial_numbers', response.trackable); + clearField('serial_numbers'); + } + } + ); } }, ]; From 3bb247a1352f86e4a17ec2416b4d346c8659361e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 23:27:43 +1100 Subject: [PATCH 15/83] Create an initial stockitem output when a new build is created --- InvenTree/InvenTree/views.py | 8 ++++---- .../migrations/0024_auto_20201020_1144.py | 20 +++++++++++++++++++ InvenTree/build/models.py | 19 +++++++++++++++++- InvenTree/build/templates/build/detail.html | 15 +++++++++++++- InvenTree/build/views.py | 9 ++++----- InvenTree/stock/serializers.py | 3 ++- 6 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 InvenTree/build/migrations/0024_auto_20201020_1144.py diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index eb168ad547..6ddee71682 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -316,13 +316,13 @@ class AjaxCreateView(AjaxMixin, CreateView): - Handles form validation via AJAX POST requests """ - def pre_save(self, **kwargs): + def pre_save(self, form, request, **kwargs): """ Hook for doing something before the form is validated """ pass - def post_save(self, new_object, **kwargs): + def post_save(self, new_object, request, **kwargs): """ Hook for doing something with the created object after it is saved """ @@ -354,9 +354,9 @@ class AjaxCreateView(AjaxMixin, CreateView): if self.form.is_valid(): - self.pre_save() + self.pre_save(self.form, request) self.object = self.form.save() - self.post_save(self.object) + self.post_save(self.object, request) # Return the PK of the newly-created object data['pk'] = self.object.pk diff --git a/InvenTree/build/migrations/0024_auto_20201020_1144.py b/InvenTree/build/migrations/0024_auto_20201020_1144.py new file mode 100644 index 0000000000..2d7c649cd5 --- /dev/null +++ b/InvenTree/build/migrations/0024_auto_20201020_1144.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-10-20 11:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0051_bomitem_optional'), + ('build', '0023_auto_20201020_1009'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='part', + field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index ca4bb586fe..3ee6aad426 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -111,7 +111,6 @@ class Build(MPTTModel): on_delete=models.CASCADE, related_name='builds', limit_choices_to={ - 'is_template': False, 'assembly': True, 'active': True, 'virtual': False, @@ -226,6 +225,24 @@ class Build(MPTTModel): return new_ref + def createInitialStockItem(self, user): + """ + Create an initial output StockItem to be completed by this build. + """ + + output = StockModels.StockItem.objects.create( + part=self.part, # Link to the parent part + location=None, # No location (yet) until it is completed + quantity=self.quantity, + batch='', # The 'batch' code is not set until the item is completed + build=self, # Point back to this build + is_building=True, # Mark this StockItem as building + ) + + output.save() + + # TODO - Add a transaction note to the new StockItem + @transaction.atomic def cancelBuild(self, user): """ Mark the Build as CANCELLED diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 6abbc69bc5..7d4c21aa5b 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -33,7 +33,20 @@ {% if build.take_from %} {{ build.take_from }} {% else %} - {% trans "Stock can be taken from any available location." %} + {% trans "Stock can be taken from any available location." %} + {% endif %} + + + + + {% trans "Destination" %} + + {% if build.destination %} + + {{ build.destination }} + + {% else %} + {% trans "Destination location not specified" %} {% endif %} diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 02e4a8d735..e8aeccab36 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -439,15 +439,14 @@ class BuildCreate(AjaxCreateView): return { 'success': _('Created new build'), } - - def post_save(self, new_object): + + def post_save(self, new_object, request, **kwargs): """ - Called immediately after the build has been created. + Called immediately after a new Build object is created. """ build = new_object - - print("Created:", build) + build.createInitialStockItem(request.user) class BuildUpdate(AjaxUpdateView): diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index d257f12f97..5bea1c4aae 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -154,8 +154,9 @@ class StockItemSerializer(InvenTreeModelSerializer): 'allocated', 'batch', 'belongs_to', - 'customer', + 'build', 'build_order', + 'customer', 'in_stock', 'is_building', 'link', From fd6d6300378024643ac2fc159c66a5a36fccfece Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 23:45:36 +1100 Subject: [PATCH 16/83] Improve grouping in Stock table --- InvenTree/templates/js/stock.html | 93 ++++++++++++++--------- InvenTree/templates/js/table_filters.html | 5 ++ 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index 150765a305..df5f172d9e 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -257,6 +257,56 @@ function loadStockTable(table, options) { filters[key] = params[key]; } + function locationDetail(row) { + /* + * Function to display a "location" of a StockItem. + * + * Complicating factors: A StockItem may not actually *be* in a location! + * - Could be at a customer + * - Could be installed in another stock item + * - Could be assigned to a sales order + * - Could be currently in production! + * + * So, instead of being naive, we'll check! + */ + + // Display text + var text = ''; + + // URL (optional) + var url = ''; + + if (row.belongs_to) { + // StockItem is installed inside a different StockItem + text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`; + url = `/stock/item/${row.belongs_to}/installed/`; + } else if (row.customer) { + // StockItem has been assigned to a customer + text = "{% trans "Shipped to customer" %}"; + url = `/company/${row.customer}/assigned-stock/`; + } else if (row.sales_order) { + // StockItem has been assigned to a sales order + text = "{% trans "Assigned to Sales Order" %}"; + url = `/order/sales-order/${row.sales_order}/`; + } else if (row.is_building && row.build) { + // StockItem is currently being built! + text = "{% trans "In production" %}"; + url = `/build/${row.build}/`; + } else if (row.location) { + text = row.location_detail.pathstring; + url = `/stock/location/${row.location}/`; + } else { + text = "{% trans "No stock location set" %}"; + url = ''; + } + + if (url) { + return renderLink(text, url); + } else { + return text; + } + } + table.inventreeTable({ method: 'get', formatNoMatches: function() { @@ -353,28 +403,20 @@ function loadStockTable(table, options) { data.forEach(function(item) { - var loc = null; + var detail = locationDetail(item); - if (item.location_detail) { - loc = item.location_detail.pathstring; - } else { - loc = "{% trans "Undefined location" %}"; - } - - if (!locations.includes(loc)) { - locations.push(loc); + if (!locations.includes(detail)) { + locations.push(detail); } }); - if (locations.length > 1) { + if (locations.length == 1) { + // Single location, easy! + return locations[0]; + } else if (locations.length > 1) { return "In " + locations.length + " locations"; } else { - // A single location! - if (row.location_detail) { - return renderLink(row.location_detail.pathstring, `/stock/location/${row.location}/`); - } else { - return "{% trans "Undefined location" %}"; - } + return "{% trans "Undefined location" %}"; } } else if (field == 'notes') { var notes = []; @@ -519,24 +561,7 @@ function loadStockTable(table, options) { title: '{% trans "Location" %}', sortable: true, formatter: function(value, row, index, field) { - if (row.belongs_to) { - var text = "{% trans 'Installed in Stock Item ' %}" + row.belongs_to; - var url = `/stock/item/${row.belongs_to}/installed/`; - - return renderLink(text, url); - } else if (row.customer) { - var text = "{% trans "Shipped to customer" %}"; - return renderLink(text, `/company/${row.customer}/assigned-stock/`); - } else if (row.sales_order) { - var text = `{% trans "Assigned to sales order" %}`; - return renderLink(text, `/order/sales-order/${row.sales_order}/`); - } - else if (value) { - return renderLink(value, `/stock/location/${row.location}/`); - } - else { - return '{% trans "No stock location set" %}'; - } + return locationDetail(row); } }, { diff --git a/InvenTree/templates/js/table_filters.html b/InvenTree/templates/js/table_filters.html index a97d358828..9b73c3f311 100644 --- a/InvenTree/templates/js/table_filters.html +++ b/InvenTree/templates/js/table_filters.html @@ -65,6 +65,11 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "In Stock" %}', description: '{% trans "Show items which are in stock" %}', }, + is_building: { + type: 'bool', + title: '{% trans "In Production" %}', + description: '{% trans "Show items which are in production" %}', + }, installed: { type: 'bool', title: '{% trans "Installed" %}', From e02536071d421305de339c012fafbb1a4f149ed5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 23:59:37 +1100 Subject: [PATCH 17/83] Add a "completed" field to the Build model - Keeps track of how many outputs have been produced - Will not be directly editable by the user --- .../migrations/0025_auto_20201020_1248.py | 24 ++ InvenTree/build/models.py | 23 +- InvenTree/build/serializers.py | 4 +- .../build/templates/build/build_base.html | 24 +- InvenTree/build/templates/build/detail.html | 207 +++++++++++------- InvenTree/stock/models.py | 2 +- .../stock/templates/stock/item_base.html | 23 +- 7 files changed, 183 insertions(+), 124 deletions(-) create mode 100644 InvenTree/build/migrations/0025_auto_20201020_1248.py diff --git a/InvenTree/build/migrations/0025_auto_20201020_1248.py b/InvenTree/build/migrations/0025_auto_20201020_1248.py new file mode 100644 index 0000000000..ecc0b73ac9 --- /dev/null +++ b/InvenTree/build/migrations/0025_auto_20201020_1248.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.7 on 2020-10-20 12:48 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0024_auto_20201020_1144'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='completed', + field=models.PositiveIntegerField(default=0, help_text='Number of stock items which have been completed', verbose_name='Completed items'), + ), + migrations.AlterField( + model_name='build', + name='quantity', + field=models.PositiveIntegerField(default=1, help_text='Number of stock items to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 3ee6aad426..a22aae196f 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -62,21 +62,6 @@ class Build(MPTTModel): def get_absolute_url(self): return reverse('build-detail', kwargs={'pk': self.id}) - - def clean(self): - """ - Validation for Build object. - """ - - super().clean() - - # Build quantity must be an integer - # Maybe in the future this will be adjusted? - - if not self.quantity == int(self.quantity): - raise ValidationError({ - 'quantity': _("Build quantity must be integer value for trackable parts") - }) reference = models.CharField( unique=True, @@ -149,7 +134,13 @@ class Build(MPTTModel): verbose_name=_('Build Quantity'), default=1, validators=[MinValueValidator(1)], - help_text=_('Number of parts to build') + help_text=_('Number of stock items to build') + ) + + completed = models.PositiveIntegerField( + verbose_name=_('Completed items'), + default=0, + help_text=_('Number of stock items which have been completed') ) status = models.PositiveIntegerField( diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 53e75942f0..6367673ce9 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -38,6 +38,7 @@ class BuildSerializer(InvenTreeModelSerializer): 'url', 'title', 'creation_date', + 'completed', 'completion_date', 'part', 'part_detail', @@ -51,9 +52,10 @@ class BuildSerializer(InvenTreeModelSerializer): ] read_only_fields = [ - 'status', + 'completed', 'creation_date', 'completion_data', + 'status', 'status_text', ] diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index a366459908..3a94398e87 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -68,11 +68,6 @@ src="{% static 'img/blank_image.png' %}"

{% trans "Build Details" %}

- - - - - @@ -88,6 +83,11 @@ src="{% static 'img/blank_image.png' %}" + + + + + {% if build.parent %} @@ -102,20 +102,6 @@ src="{% static 'img/blank_image.png' %}" {% endif %} - - - - -
{% trans "Build Order Reference" %}{{ build }}
{% trans "Part" %}{% trans "Status" %} {% build_status_label build.status %}
{% trans "Progress" %} {{ build.completed }} / {{ build.quantity }}
{{ build.sales_order }}
{% trans "BOM Price" %} - {% if bom_price %} - {{ bom_price }} - {% if build.part.has_complete_bom_pricing == False %} -
{% trans "BOM pricing is incomplete" %} - {% endif %} - {% else %} - {% trans "No pricing information" %} - {% endif %} -
{% endblock %} diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 7d4c21aa5b..b51d12f772 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -10,90 +10,133 @@
- - - - - - - - - - - - - - - - - - - - + + + + + {% endif %} + + + + + +
{% trans "Title" %}{{ build.title }}
{% trans "Part" %}{{ build.part.full_name }}
{% trans "Quantity" %}{{ build.quantity }}
{% trans "Stock Source" %} - {% if build.take_from %} - {{ build.take_from }} - {% else %} - {% trans "Stock can be taken from any available location." %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if build.batch %} + + + + + {% endif %} - - - - - - + + + + {% endif %} - - - - - - - -{% if build.batch %} - - - - - -{% endif %} -{% if build.link %} - - - - - -{% endif %} - - - - - -{% if build.is_active %} - - - - + + + + {% endif %} - - -{% endif %} -{% if build.completion_date %} - - - - - -{% endif %} -
{% trans "Description" %}{{ build.title }}
{% trans "Part" %}{{ build.part.full_name }}
{% trans "Quantity" %}{{ build.quantity }}
{% trans "Stock Source" %} + {% if build.take_from %} + {{ build.take_from }} + {% else %} + {% trans "Stock can be taken from any available location." %} + {% endif %} +
{% trans "Destination" %} + {% if build.destination %} + + {{ build.destination }} + + {% else %} + {% trans "Destination location not specified" %} + {% endif %} +
{% trans "Status" %}{% build_status_label build.status %}
{% trans "Progress" %}{{ build.completed }} / {{ build.quantity }}
{% trans "Batch" %}{{ build.batch }}
{% trans "Destination" %} - {% if build.destination %} - - {{ build.destination }} - - {% else %} - {% trans "Destination location not specified" %} + {% if build.parent %} +
{% trans "Parent Build" %}{{ build.parent }}
{% trans "Status" %}{% build_status_label build.status %}
{% trans "Batch" %}{{ build.batch }}
{% trans "External Link" %}{{ build.link }}
{% trans "Created" %}{{ build.creation_date }}
{% trans "Enough Parts?" %} - {% if build.can_build %} - {% trans "Yes" %} - {% else %} - {% trans "No" %} + {% if build.sales_order %} +
{% trans "Sales Order" %}{{ build.sales_order }}
{% trans "Completed" %}{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %}
+ {% if build.link %} +
{% trans "External Link" %}{{ build.link }}
{% trans "Created" %}{{ build.creation_date }}
+ +
+ + + + + + + + {% if build.is_active %} + + + + + + {% endif %} + {% if build.completion_date %} + + + + + + {% endif %} + +
{% trans "BOM Price" %} + {% if bom_price %} + {{ bom_price }} + {% if build.part.has_complete_bom_pricing == False %} +
{% trans "BOM pricing is incomplete" %} + {% endif %} + {% else %} + {% trans "No pricing information" %} + {% endif %} +
{% trans "Enough Parts?" %} + {% if build.can_build %} + {% trans "Yes" %} + {% else %} + {% trans "No" %} + {% endif %} +
{% trans "Completed" %}{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %}
+
+ {% endblock %} diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1535ded420..f9079e1936 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -130,7 +130,7 @@ class StockItem(MPTTModel): status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) notes: Extra notes field build: Link to a Build (if this stock item was created from a build) - is_building: Boolean field indicating if this stock item is currently being built + is_building: Boolean field indicating if this stock item is currently being built (or is "in production") purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index d7eb987aab..2538dbd398 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,6 +15,20 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} +{% if item.is_building %} +
+ {% trans "This stock item is in production and cannot be edited." %}
+ {% trans "Edit the stock item from the build view." %}
+ + {% if item.build %} + + {{ item.build }} + + {% endif %} + +
+{% endif %} + {% if item.hasRequiredTests and not item.passedAllRequiredTests %}
{% trans "This stock item has not passed all required tests" %} @@ -79,7 +93,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
-
@@ -99,7 +112,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
- {% if roles.stock.change %} + {% if roles.stock.change and not item.is_building %}
{% endif %} - {% if roles.stock.change %} + {% if roles.stock.change and not item.is_building %}
{% endif %}
- -
+ + + + +
+ {% for item in build.incomplete_outputs %} + {% include "build/allocation_card.html" with item=item complete=False %} + {% endfor %} +
{% endblock %} {% block js_ready %} {{ block.super }} + {% for item in build.incomplete_outputs %} + + // Get the build output as a javascript object + inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, + { + success: function(response) { + loadBuildOutputAllocationTable( + {{ build.pk }}, + {{ build.part.pk }}, + response + ); + } + } + ); + {% endfor %} + var buildTable = $("#build-item-list"); // Calculate sum of allocations for a particular table row diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html new file mode 100644 index 0000000000..c00eaf6bbf --- /dev/null +++ b/InvenTree/build/templates/build/allocation_card.html @@ -0,0 +1,29 @@ +{% load i18n %} + +
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/InvenTree/build/templates/build/create_build_item.html b/InvenTree/build/templates/build/create_build_item.html index c08b987d36..8f58e884d6 100644 --- a/InvenTree/build/templates/build/create_build_item.html +++ b/InvenTree/build/templates/build/create_build_item.html @@ -1,9 +1,22 @@ {% extends "modal_form.html" %} +{% load i18n %} {% block pre_form_content %} +
+

+ {% trans "Select a stock item to allocate to the selected build output" %} +

+ {% if output %} +

+ {% trans "The allocated stock will be installed into the following build output:" %} +
+ {{ output }} +

+ {% endif %} +
{% if no_stock %} {% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index e8aeccab36..8303c88f9f 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -492,23 +492,37 @@ class BuildItemDelete(AjaxDeleteView): class BuildItemCreate(AjaxCreateView): - """ View for allocating a new part to a build """ + """ + View for allocating a StockItems to a build output. + """ model = BuildItem form_class = forms.EditBuildItemForm ajax_template_name = 'build/create_build_item.html' - ajax_form_title = _('Allocate new Part') + ajax_form_title = _('Allocate stock to build output') role_required = 'build.add' + # The output StockItem against which the allocation is being made + output = None + + # The "part" which is being allocated to the output part = None + available_stock = None def get_context_data(self): - ctx = super(AjaxCreateView, self).get_context_data() + """ + Provide context data to the template which renders the form. + """ + + ctx = super().get_context_data() if self.part: ctx['part'] = self.part + if self.output: + ctx['output'] = self.output + if self.available_stock: ctx['stock'] = self.available_stock else: @@ -526,7 +540,28 @@ class BuildItemCreate(AjaxCreateView): build_id = form['build'].value() if build_id is not None: + """ + If the build has been provided, hide the widget to change the build selection. + Additionally, update the allowable selections for other fields. + """ form.fields['build'].widget = HiddenInput() + form.fields['install_into'].queryset = StockItem.objects.filter(build=build_id, is_building=True) + else: + """ + Build has *not* been selected + """ + pass + + # If the output stock item is specified, hide the input field + output_id = form['install_into'].value() + + if output_id is not None: + + try: + self.output = StockItem.objects.get(pk=output_id) + form.fields['install_into'].widget = HiddenInput() + except (ValueError, StockItem.DoesNotExist): + pass # If the sub_part is supplied, limit to matching stock items part_id = self.get_param('part') @@ -577,12 +612,15 @@ class BuildItemCreate(AjaxCreateView): """ Provide initial data for BomItem. Look for the folllowing in the GET data: - build: pk of the Build object + - part: pk of the Part object which we are assigning + - output: pk of the StockItem object into which the allocated stock will be installed """ initials = super(AjaxCreateView, self).get_initial().copy() build_id = self.get_param('build') part_id = self.get_param('part') + output_id = self.get_param('install_into') # Reference to a Part object part = None @@ -593,6 +631,9 @@ class BuildItemCreate(AjaxCreateView): # Reference to a Build object build = None + # Reference to a StockItem object + output = None + if part_id: try: part = Part.objects.get(pk=part_id) @@ -623,7 +664,7 @@ class BuildItemCreate(AjaxCreateView): if item_id: try: item = StockItem.objects.get(pk=item_id) - except: + except (ValueError, StockItem.DoesNotExist): pass # If a StockItem is not selected, try to auto-select one @@ -639,6 +680,17 @@ class BuildItemCreate(AjaxCreateView): else: quantity = min(quantity, item.unallocated_quantity()) + # If the output has been specified + print("output_id:", output_id) + if output_id: + try: + output = StockItem.objects.get(pk=output_id) + initials['install_into'] = output + print("Output:", output) + except (ValueError, StockItem.DoesNotExist): + pass + print("no output found") + if quantity is not None: initials['quantity'] = quantity diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d427409c71..b289b3e0ad 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -934,8 +934,10 @@ class Part(MPTTModel): def required_parts(self): """ Return a list of parts required to make this part (list of BOM items) """ parts = [] + for bom in self.bom_items.all().select_related('sub_part'): parts.append(bom.sub_part) + return parts def get_allowed_bom_items(self): diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 886657a2bc..4118e6ed54 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -29,9 +29,155 @@ function newBuildOrder(options={}) { ], } ) - } + +function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { + /* + * Load the "allocation table" for a particular build output. + * + * Args: + * - buildId: The PK of the Build object + * - partId: The PK of the Part object + * - output: The StockItem object which is the "output" of the build + * - options: + * -- table: The #id of the table (will be auto-calculated if not provided) + */ + + var outputId = output.pk; + + var table = options.table || `#allocation-table-${outputId}`; + + function reloadTable() { + // Reload the entire build allocation table + $(table).bootstrapTable('refresh'); + } + + function setupCallbacks() { + // Register button callbacks once table data are loaded + + // Callback for 'allocate' button + $(table).find(".button-add").click(function() { + + // Primary key of the 'sub_part' + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = $(table).bootstrapTable('getData')[idx]; + + // Launch form to allocate new stock against this output + launchModalForm("{% url 'build-item-create' %}", { + success: reloadTable, + data: { + part: pk, + build: buildId, + install_into: outputId, + }, + secondary: [ + { + field: 'stock_item', + label: '{% trans "New Stock Item" %}', + title: '{% trans "Create new Stock Item" %}', + url: '{% url "stock-item-create" %}', + data: { + part: pk, + }, + }, + ] + }); + }); + } + + // Load table of BOM items + $(table).inventreeTable({ + url: "{% url 'api-bom-list' %}", + queryParams: { + part: partId, + sub_part_detail: true, + }, + formatNoMatches: function() { + return "{% trans "No BOM items found" %}"; + }, + name: 'build-allocation', + onPostBody: setupCallbacks, + onLoadSuccess: function(tableData) { + // Once the BOM data are loaded, request allocation data for this build output + + inventreeGet('/api/build/item/', + { + build: buildId, + output: outputId, + }, + { + success: function(data) { + // TODO + } + } + ); + }, + showColumns: false, + detailViewByClick: true, + detailView: true, + detailFilter: function(index, row) { + return row.allocations != null; + }, + detailFormatter: function(index, row, element) { + // TODO + return '---'; + }, + columns: [ + { + field: 'pk', + visible: false, + }, + { + field: 'sub_part_detail.full_name', + title: "{% trans "Required Part" %}", + sortable: true, + formatter: function(value, row, index, field) { + var url = `/part/${row.sub_part}/`; + var thumb = row.sub_part_detail.thumbnail; + var name = row.sub_part_detail.full_name; + + var html = imageHoverIcon(thumb) + renderLink(name, url); + + return html; + } + }, + { + field: 'reference', + title: '{% trans "Reference" %}', + }, + { + field: 'allocated', + title: '{% trans "Allocated" %}', + formatter: function(value, row, index, field) { + var allocated = value || 0; + var required = row.quantity * output.quantity; + + return makeProgressBar(allocated, required); + } + }, + { + field: 'actions', + title: '{% trans "Actions" %}', + formatter: function(value, row, index, field) { + // Generate action buttons for this build output + var html = `
`; + + html += makeIconButton('fa-plus icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}'); + + html += '
'; + + return html; + } + }, + ] + }); +} + + function loadBuildTable(table, options) { // Display a table of Build objects From d37cdd8e50471b48381363d32781557d030684aa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 22 Oct 2020 23:49:23 +1100 Subject: [PATCH 23/83] Improved filtering for stockitems going into a buildallocation --- InvenTree/build/views.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 8303c88f9f..ea4d1905d4 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -566,14 +566,18 @@ class BuildItemCreate(AjaxCreateView): # If the sub_part is supplied, limit to matching stock items part_id = self.get_param('part') + # We need to precisely control which StockItem objects the user can choose to allocate + stock_filter = form.fields['stock_item'].queryset + + # Restrict to only items which are "in stock" + stock_filter = stock_filter.filter(StockItem.IN_STOCK_FILTER) + if part_id: try: self.part = Part.objects.get(pk=part_id) - - query = form.fields['stock_item'].queryset - + # Only allow StockItem objects which match the current part - query = query.filter(part=part_id) + stock_filter = stock_filter.filter(part=part_id) if build_id is not None: try: @@ -581,31 +585,26 @@ class BuildItemCreate(AjaxCreateView): if build.take_from is not None: # Limit query to stock items that are downstream of the 'take_from' location - query = query.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()]) + stock_filter = stock_filter.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()]) except Build.DoesNotExist: pass # Exclude StockItem objects which are already allocated to this build and part - 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() - self.available_stock = stocks - - # 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 + stock_filter = stock_filter.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)]) except Part.DoesNotExist: self.part = None pass + form.fields['stock_item'].query = stock_filter + + self.available_stock = stock_filter.all() + + # If there is only a single stockitem available, select it! + if len(self.available_stock) == 1: + form.fields['stock_item'].initial = self.available_stock[0].pk + return form def get_initial(self): From ae20db0ec6d1aa1a68b2c314a79ef8bac8f2f09d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 22 Oct 2020 23:57:07 +1100 Subject: [PATCH 24/83] Add actions for the sub-table allocation list --- InvenTree/templates/js/build.html | 136 +++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 4118e6ed54..0169fc0276 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -100,6 +100,7 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { return "{% trans "No BOM items found" %}"; }, name: 'build-allocation', + uniqueId: 'sub_part', onPostBody: setupCallbacks, onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output @@ -111,11 +112,38 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { }, { success: function(data) { - // TODO + // Iterate through the returned data, and group by the part they point to + var allocations = {}; + + data.forEach(function(item) { + + // Group BuildItem objects by part + var part = item.part; + var key = parseInt(part); + + if (!(key in allocations)) { + allocations[key] = new Array(); + } + + allocations[key].push(item); + }); + + // Now update the allocations for each row in the table + for (var key in allocations) { + // Select the associated row in the table + var tableRow = $(table).bootstrapTable('getRowByUniqueId', key); + + // Set the allocation list for that row + tableRow.allocations = allocations[key]; + + // Push the updated row back into the main table + $(table).bootstrapTable('updateByUniqueId', key, tableRow, true); + } } } ); }, + sortable: true, showColumns: false, detailViewByClick: true, detailView: true, @@ -123,8 +151,99 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { return row.allocations != null; }, detailFormatter: function(index, row, element) { - // TODO - return '---'; + // Contruct an 'inner table' which shows which stock items have been allocated + + var subTableId = `allocation-table-${row.pk}`; + + var html = `
`; + + element.html(html); + + var lineItem = row; + + var subTable = $(`#${subTableId}`); + + subTable.bootstrapTable({ + data: row.allocations, + showHeader: true, + columns: [ + { + width: '50%', + field: 'quantity', + title: '{% trans "Assigned Stock" %}', + formatter: function(value, row, index, field) { + var text = ''; + + var url = ''; + + if (row.serial && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + {% if build.status == BuildStatus.COMPLETE %} + url = `/stock/item/${row.pk}/`; + {% else %} + url = `/stock/item/${row.stock_item}/`; + {% endif %} + + return renderLink(text, url); + } + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row, index, field) { + if (row.stock_item_detail.location) { + var text = row.stock_item_detail.location_name; + var url = `/stock/location/${row.stock_item_detail.location}/`; + + return renderLink(text, url); + } else { + return '{% trans "No location set" %}'; + } + } + }, + { + field: 'actions', + formatter: function(value, row, index, field) { + /* Actions available for a particular stock item allocation: + * + * - Edit the allocation quantity + * - Delete the allocation + */ + + var pk = row.pk; + + var html = `
`; + + html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + + html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + + html += `
`; + + return html; + } + } + ] + }); + + // Assign button callbacks to the newly created allocation buttons + subTable.find('.button-allocation-edit').click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/edit/`, { + success: reloadTable, + }); + }); + + subTable.find('.button-allocation-delete').click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/delete/`, { + success: reloadTable, + }); + }); }, columns: [ { @@ -148,12 +267,21 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { { field: 'reference', title: '{% trans "Reference" %}', + sortable: true, }, { field: 'allocated', title: '{% trans "Allocated" %}', + sortable: true, formatter: function(value, row, index, field) { - var allocated = value || 0; + var allocated = 0; + + if (row.allocations) { + row.allocations.forEach(function(item) { + allocated += item.quantity; + }); + } + var required = row.quantity * output.quantity; return makeProgressBar(allocated, required); From 23ac83d2a8165529419dc186e02896a0df1174bc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 22 Oct 2020 23:59:21 +1100 Subject: [PATCH 25/83] Change extension on "dynamic" js files - Yay, the editor now highlights code properly! --- InvenTree/InvenTree/urls.py | 16 ++++++++-------- .../templates/js/{barcode.html => barcode.js} | 0 InvenTree/templates/js/{bom.html => bom.js} | 0 InvenTree/templates/js/{build.html => build.js} | 0 .../templates/js/{company.html => company.js} | 0 InvenTree/templates/js/{order.html => order.js} | 0 InvenTree/templates/js/{part.html => part.js} | 0 InvenTree/templates/js/{stock.html => stock.js} | 0 .../js/{table_filters.html => table_filters.js} | 0 9 files changed, 8 insertions(+), 8 deletions(-) rename InvenTree/templates/js/{barcode.html => barcode.js} (100%) rename InvenTree/templates/js/{bom.html => bom.js} (100%) rename InvenTree/templates/js/{build.html => build.js} (100%) rename InvenTree/templates/js/{company.html => company.js} (100%) rename InvenTree/templates/js/{order.html => order.js} (100%) rename InvenTree/templates/js/{part.html => part.js} (100%) rename InvenTree/templates/js/{stock.html => stock.js} (100%) rename InvenTree/templates/js/{table_filters.html => table_filters.js} (100%) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 0b4292cbd9..7015e00d14 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -84,14 +84,14 @@ settings_urls = [ # Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ - url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.html'), name='barcode.js'), - url(r'^part.js', DynamicJsView.as_view(template_name='js/part.html'), name='part.js'), - url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.html'), name='stock.js'), - url(r'^build.js', DynamicJsView.as_view(template_name='js/build.html'), name='build.js'), - url(r'^order.js', DynamicJsView.as_view(template_name='js/order.html'), name='order.js'), - url(r'^company.js', DynamicJsView.as_view(template_name='js/company.html'), name='company.js'), - url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.html'), name='bom.js'), - url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.html'), name='table_filters.js'), + url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'), + url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'), + url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'), + url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'), + url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'), + url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'), + url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'), + url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'), ] urlpatterns = [ diff --git a/InvenTree/templates/js/barcode.html b/InvenTree/templates/js/barcode.js similarity index 100% rename from InvenTree/templates/js/barcode.html rename to InvenTree/templates/js/barcode.js diff --git a/InvenTree/templates/js/bom.html b/InvenTree/templates/js/bom.js similarity index 100% rename from InvenTree/templates/js/bom.html rename to InvenTree/templates/js/bom.js diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.js similarity index 100% rename from InvenTree/templates/js/build.html rename to InvenTree/templates/js/build.js diff --git a/InvenTree/templates/js/company.html b/InvenTree/templates/js/company.js similarity index 100% rename from InvenTree/templates/js/company.html rename to InvenTree/templates/js/company.js diff --git a/InvenTree/templates/js/order.html b/InvenTree/templates/js/order.js similarity index 100% rename from InvenTree/templates/js/order.html rename to InvenTree/templates/js/order.js diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.js similarity index 100% rename from InvenTree/templates/js/part.html rename to InvenTree/templates/js/part.js diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.js similarity index 100% rename from InvenTree/templates/js/stock.html rename to InvenTree/templates/js/stock.js diff --git a/InvenTree/templates/js/table_filters.html b/InvenTree/templates/js/table_filters.js similarity index 100% rename from InvenTree/templates/js/table_filters.html rename to InvenTree/templates/js/table_filters.js From 6245d65ebca59ee4c5f7bd815650416d2caa4c4b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 23 Oct 2020 00:08:40 +1100 Subject: [PATCH 26/83] Tweaks --- InvenTree/build/models.py | 6 +++--- .../build/templates/build/edit_build_item.html | 10 ++++++++++ InvenTree/build/views.py | 15 +++++---------- InvenTree/templates/js/build.js | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 InvenTree/build/templates/build/edit_build_item.html diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 166d70afac..c42f52ec1a 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -22,7 +22,7 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey from InvenTree.status_codes import BuildStatus -from InvenTree.helpers import increment, getSetting +from InvenTree.helpers import increment, getSetting, normalize from InvenTree.validators import validate_build_order_reference import InvenTree.fields @@ -589,8 +589,8 @@ class BuildItem(models.Model): if self.quantity > self.stock_item.quantity: errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format( - n=self.quantity, - q=self.stock_item.quantity + n=normalize(self.quantity), + q=normalize(self.stock_item.quantity) ))] if self.stock_item.quantity - self.stock_item.allocation_count() + self.quantity < self.quantity: diff --git a/InvenTree/build/templates/build/edit_build_item.html b/InvenTree/build/templates/build/edit_build_item.html new file mode 100644 index 0000000000..99cad71ba2 --- /dev/null +++ b/InvenTree/build/templates/build/edit_build_item.html @@ -0,0 +1,10 @@ +{% extends "modal_form.html" %} +{% load i18n %} + +{% block pre_form_content %} +
+

+ {% trans "Alter the quantity of stock allocated to the build output" %} +

+
+{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index ea4d1905d4..3d3a03b284 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -700,7 +700,7 @@ class BuildItemEdit(AjaxUpdateView): """ View to edit a BuildItem object """ model = BuildItem - ajax_template_name = 'modal_form.html' + ajax_template_name = 'build/edit_build_item.html' form_class = forms.EditBuildItemForm ajax_form_title = _('Edit Stock Allocation') role_required = 'build.change' @@ -720,14 +720,9 @@ class BuildItemEdit(AjaxUpdateView): form = super(BuildItemEdit, self).get_form() - query = StockItem.objects.all() - - if build_item.stock_item: - part_id = build_item.stock_item.part.id - query = query.filter(part=part_id) - - form.fields['stock_item'].queryset = query - - form.fields['build'].widget = HiddenInput() + # Hide fields which we do not wish the user to edit + for field in ['build', 'stock_item', 'install_into']: + if form[field].value(): + form.fields[field].widget = HiddenInput() return form diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 0169fc0276..1b330f63b5 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -252,7 +252,7 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { }, { field: 'sub_part_detail.full_name', - title: "{% trans "Required Part" %}", + title: '{% trans "Required Part" %}', sortable: true, formatter: function(value, row, index, field) { var url = `/part/${row.sub_part}/`; @@ -326,7 +326,7 @@ function loadBuildTable(table, options) { $(table).inventreeTable({ method: 'get', formatNoMatches: function() { - return "{% trans "No builds matching query" %}"; + return '{% trans "No builds matching query" %}'; }, url: options.url, queryParams: filters, From 33c454ed5a200d1a5ba976b98038d0446be72f0e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 23 Oct 2020 00:51:01 +1100 Subject: [PATCH 27/83] Add action buttons to each build output --- .../templates/build/allocation_card.html | 12 ++- .../templates/build/delete_build_item.html | 11 ++- InvenTree/build/views.py | 3 - InvenTree/templates/js/build.js | 89 ++++++++++++++++++- 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html index c00eaf6bbf..6e9faceb22 100644 --- a/InvenTree/build/templates/build/allocation_card.html +++ b/InvenTree/build/templates/build/allocation_card.html @@ -1,6 +1,6 @@ {% load i18n %} -
+