From 34ff05d66e85c6e4720a3322a83929e363743426 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 30 Oct 2020 16:09:27 -0500 Subject: [PATCH 01/20] Created PartCategoryParameterTemplate model and admin interface --- InvenTree/part/admin.py | 14 +++++ .../0053_partcategoryparametertemplate.py | 27 +++++++++ InvenTree/part/models.py | 60 ++++++++++++++++++- 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 InvenTree/part/migrations/0053_partcategoryparametertemplate.py diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index f0c9e3f233..61097d0952 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -12,6 +12,7 @@ from .models import PartCategory, Part from .models import PartAttachment, PartStar from .models import BomItem from .models import PartParameterTemplate, PartParameter +from .models import PartCategoryParameterTemplate from .models import PartTestTemplate from .models import PartSellPriceBreak @@ -269,6 +270,18 @@ class ParameterAdmin(ImportExportModelAdmin): list_display = ('part', 'template', 'data') +class PartCategoryParameterAdmin(admin.ModelAdmin): + + def get_form(self, request, obj=None, **kwargs): + """ Display only parent categories as choices for category field """ + + form = super().get_form(request, obj, **kwargs) + + form.base_fields['category'].choices = PartCategory.get_parent_categories() + + return form + + class PartSellPriceBreakAdmin(admin.ModelAdmin): class Meta: @@ -284,5 +297,6 @@ admin.site.register(PartStar, PartStarAdmin) admin.site.register(BomItem, BomItemAdmin) admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) admin.site.register(PartParameter, ParameterAdmin) +admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin) admin.site.register(PartTestTemplate, PartTestTemplateAdmin) admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin) diff --git a/InvenTree/part/migrations/0053_partcategoryparametertemplate.py b/InvenTree/part/migrations/0053_partcategoryparametertemplate.py new file mode 100644 index 0000000000..e9a1e90af7 --- /dev/null +++ b/InvenTree/part/migrations/0053_partcategoryparametertemplate.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.7 on 2020-10-30 18:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0052_auto_20201027_1557'), + ] + + operations = [ + migrations.CreateModel( + name='PartCategoryParameterTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('default_value', models.CharField(blank=True, help_text='Default Parameter Value', max_length=500)), + ('category', models.ForeignKey(help_text='Part Category', on_delete=django.db.models.deletion.CASCADE, related_name='parameter_templates', to='part.PartCategory')), + ('parameter_template', models.ForeignKey(help_text='Parameter Template', on_delete=django.db.models.deletion.CASCADE, related_name='part_categories', to='part.PartParameterTemplate')), + ], + ), + migrations.AddConstraint( + model_name='partcategoryparametertemplate', + constraint=models.UniqueConstraint(fields=('category', 'parameter_template'), name='unique_category_parameter_template_pair'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 715d1423eb..2278993bf7 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction -from django.db.models import Sum +from django.db.models import Sum, UniqueConstraint from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator @@ -163,6 +163,22 @@ class PartCategory(InvenTreeTree): return category_parameters + @classmethod + def get_parent_categories(cls): + """ Return tuple list of parent (root) categories """ + + # Store parent categories (add empty label) + parent_categories = [ + ('', '-' * 10) + ] + # Get root nodes + root_categories = cls.objects.filter(level=0) + + for category in root_categories: + parent_categories.append((category.id, category.name)) + + return parent_categories + @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') def before_delete_part_category(sender, instance, using, **kwargs): @@ -1550,6 +1566,48 @@ class PartParameter(models.Model): return part_parameter +class PartCategoryParameterTemplate(models.Model): + """ + A PartCategoryParameterTemplate creates a unique relationship between a PartCategory + and a PartParameterTemplate. + Multiple PartParameterTemplate instances can be associated to a PartCategory to drive + a default list of parameter templates attached to a Part instance upon creation. + + Attributes: + category: Reference to a single PartCategory object + parameter_template: Reference to a single PartParameterTemplate object + default_value: The default value for the parameter in the context of the selected + category + """ + + class Meta: + constraints = [ + UniqueConstraint(fields=['category', 'parameter_template'], + name='unique_category_parameter_template_pair') + ] + + def __str__(self): + """ String representation of a PartCategoryParameterTemplate (admin interface) """ + if self.default_value: + return f'{self.category.name} | {self.parameter_template.name} | {self.default_value}' + else: + return f'{self.category.name} | {self.parameter_template.name}' + + category = models.ForeignKey(PartCategory, + on_delete=models.CASCADE, + related_name='parameter_templates', + help_text=_('Part Category')) + + parameter_template = models.ForeignKey(PartParameterTemplate, + on_delete=models.CASCADE, + related_name='part_categories', + help_text=_('Parameter Template')) + + default_value = models.CharField(max_length=500, + blank=True, + help_text=_('Default Parameter Value')) + + class BomItem(models.Model): """ A BomItem links a part to its component items. A part can have a BOM (bill of materials) which defines From 5310ce84654102a0a1f1fe89420d0756f2966b64 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 30 Oct 2020 17:17:18 -0500 Subject: [PATCH 02/20] First step into managing Category Parameters to InvenTree settings --- InvenTree/InvenTree/urls.py | 1 + InvenTree/part/forms.py | 13 ++++++ InvenTree/part/urls.py | 8 ++++ InvenTree/part/views.py | 40 +++++++++++++++++++ .../InvenTree/settings/category.html | 40 +++++++++++++++++++ .../templates/InvenTree/settings/tabs.html | 3 ++ InvenTree/users/models.py | 1 + 7 files changed, 106 insertions(+) create mode 100644 InvenTree/templates/InvenTree/settings/category.html diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d729210235..9b74aabe5f 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -75,6 +75,7 @@ settings_urls = [ url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), url(r'^currency/?', SettingsView.as_view(template_name='InvenTree/settings/currency.html'), name='settings-currency'), + url(r'^category/?', SettingsView.as_view(template_name='InvenTree/settings/category.html'), name='settings-category'), url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), url(r'^build/?', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'), diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 52c39bf3ba..d1a3dcd74e 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -16,6 +16,7 @@ from django.utils.translation import ugettext as _ from .models import Part, PartCategory, PartAttachment from .models import BomItem from .models import PartParameterTemplate, PartParameter +from .models import PartCategoryParameterTemplate from .models import PartTestTemplate from .models import PartSellPriceBreak @@ -247,6 +248,18 @@ class EditCategoryForm(HelperForm): ] +class EditCategoryParameterTemplateForm(HelperForm): + """ Form for editing a PartParameterTemplate object """ + + class Meta: + model = PartCategoryParameterTemplate + fields = [ + 'category', + 'parameter_template', + 'default_value', + ] + + class EditBomItemForm(HelperForm): """ Form for editing a BomItem object """ diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 4d81faa2d6..b5881fd0b4 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -74,10 +74,18 @@ part_detail_urls = [ url(r'^.*$', views.PartDetail.as_view(), name='part-detail'), ] +category_parameter_urls = [ + url(r'^new/', views.CategoryParameterTemplateCreate.as_view(), name='category-param-template-create'), + # url(r'^(?P\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='category-param-template-edit'), + # url(r'^(?P\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='category-param-template-edit'), +] + part_category_urls = [ url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'), url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'), + url(r'^template/', include(category_parameter_urls)), + url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'), url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'), ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 0b96addc84..d13f2223b1 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -23,6 +23,7 @@ from decimal import Decimal, InvalidOperation from .models import PartCategory, Part, PartAttachment from .models import PartParameterTemplate, PartParameter +from .models import PartCategoryParameterTemplate from .models import BomItem from .models import match_part_names from .models import PartTestTemplate @@ -2137,6 +2138,45 @@ class CategoryCreate(AjaxCreateView): return initials +class CategoryParameterTemplateCreate(AjaxCreateView): + """ View for creating a new PartCategoryParameterTemplate """ + + role_required = 'part.add' + + model = PartCategoryParameterTemplate + form_class = part_forms.EditCategoryParameterTemplateForm + ajax_form_title = _('Create Category Parameter Template') + + def get_initial(self): + """ Get initial data for Category """ + initials = super().get_initial().copy() + + category_id = self.kwargs.get('pk', None) + + if category_id: + try: + initials['category'] = PartCategory.objects.get(pk=category_id) + except (PartCategory.DoesNotExist, ValueError): + pass + + return initials + + def get_form(self): + """ Create a form to upload a new CategoryParameterTemplate + - Hide the 'category' field (parent part) + - Display parameter templates which are not yet related + """ + + form = super(AjaxCreateView, self).get_form() + + form.fields['category'].widget = HiddenInput() + + if form.is_valid(): + form.cleaned_data['category'] = self.kwargs.get('pk', None) + + return form + + class BomItemDetail(InvenTreeRoleMixin, DetailView): """ Detail view for BomItem """ context_object_name = 'item' diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html new file mode 100644 index 0000000000..b643af53dd --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -0,0 +1,40 @@ +{% extends "InvenTree/settings/settings.html" %} +{% load i18n %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='part' %} +{% endblock %} + +{% block subtitle %} +{% trans "Category Settings" %} +{% endblock %} + +{% block settings %} + +

{% trans "Category Parameter Templates" %}

+ +
{% trans "Category: XXX (id = 1)" %}
+ +
+ +
+ + +
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + $("#new-param").click(function() { + launchModalForm("{% url 'category-param-template-create' 1 %}", { + success: function() { + $("#param-table").bootstrapTable('refresh'); + }, + }); + }); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index d104908c49..6991b54b95 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -18,6 +18,9 @@ {% trans "Currency" %} + + {% trans "Categories" %} + {% trans "Parts" %} diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index d3c713d07d..7e65ba248e 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_partcategoryparametertemplate', ], 'stock': [ 'stock_stockitem', From 3e5d8d2b2d23746795268beaafe28df5dbdb0c6d Mon Sep 17 00:00:00 2001 From: eeintech Date: Sat, 31 Oct 2020 08:35:47 -0500 Subject: [PATCH 03/20] Added form to select category in settings and update context data --- InvenTree/InvenTree/forms.py | 34 +++++++++++++++ InvenTree/InvenTree/urls.py | 5 ++- InvenTree/InvenTree/views.py | 41 ++++++++++++++++++- InvenTree/part/models.py | 5 +++ .../InvenTree/settings/category.html | 23 +++++++---- 5 files changed, 98 insertions(+), 10 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 80df8914d3..707c0c5c79 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -12,6 +12,7 @@ from crispy_forms.layout import Layout, Field from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div from django.contrib.auth.models import User from common.models import ColorTheme +from part.models import PartCategory class HelperForm(forms.ModelForm): @@ -195,3 +196,36 @@ class ColorThemeSelectForm(forms.ModelForm): css_class='row', ), ) + + +class SettingCategorySelectForm(forms.ModelForm): + """ Form for setting category settings """ + + name = forms.ChoiceField(choices=(), required=False) + + class Meta: + model = PartCategory + fields = [ + 'name' + ] + + def __init__(self, *args, **kwargs): + super(SettingCategorySelectForm, self).__init__(*args, **kwargs) + + # Populate category choices + self.fields['name'].choices = PartCategory.get_parent_categories() + + self.helper = FormHelper() + # Form rendering + self.helper.form_show_labels = False + self.helper.layout = Layout( + Div( + Div(Field('name'), + css_class='col-sm-6', + style='width: 200px;'), + Div(StrictButton(_('Select Category'), css_class='btn btn-primary', type='submit'), + css_class='col-sm-6', + style='width: auto;'), + css_class='row', + ), + ) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 9b74aabe5f..8006d633b5 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -36,7 +36,8 @@ from django.views.generic.base import RedirectView from rest_framework.documentation import include_docs_urls from .views import IndexView, SearchView, DatabaseStatsView -from .views import SettingsView, EditUserView, SetPasswordView, ColorThemeSelectView +from .views import SettingsView, EditUserView, SetPasswordView +from .views import ColorThemeSelectView, SettingCategorySelectView from .views import DynamicJsView from common.views import SettingEdit @@ -75,7 +76,7 @@ settings_urls = [ url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), url(r'^currency/?', SettingsView.as_view(template_name='InvenTree/settings/currency.html'), name='settings-currency'), - url(r'^category/?', SettingsView.as_view(template_name='InvenTree/settings/category.html'), name='settings-category'), + url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'), url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), url(r'^build/?', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 57f80f1be7..465b0dc194 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -24,7 +24,8 @@ from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet -from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm +from .forms import DeleteForm, EditUserForm, SetPasswordForm +from .forms import ColorThemeSelectForm, SettingCategorySelectForm from .helpers import str2bool from rest_framework import views @@ -775,6 +776,44 @@ class ColorThemeSelectView(FormView): return self.form_invalid(form) +class SettingCategorySelectView(FormView): + """ View for selecting categories in settings """ + + form_class = SettingCategorySelectForm + success_url = reverse_lazy('settings-category') + template_name = "InvenTree/settings/category.html" + + def get_initial(self): + """ Set category selection """ + + initial = super(SettingCategorySelectView, self).get_initial() + + category = self.request.GET.get('category', None) + if category: + initial['name'] = category + + return initial + + def post(self, request, *args, **kwargs): + """ Handle POST request (which contains category selection). + + Pass the selected category to the page template + """ + + form = self.get_form() + + if form.is_valid(): + category = form.cleaned_data['name'] + + context = self.get_context_data() + + context['category'] = category + + return super(SettingCategorySelectView, self).render_to_response(context) + + return self.form_invalid(form) + + class DatabaseStatsView(AjaxView): """ View for displaying database statistics """ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 2278993bf7..37788ae56a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -179,6 +179,11 @@ class PartCategory(InvenTreeTree): return parent_categories + def get_parameter_templates(self): + """ Return parameter templates associated to category """ + + return PartCategoryParameterTemplate.objects.filter(category=self.id) + @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') def before_delete_part_category(sender, instance, using, **kwargs): diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html index b643af53dd..3dc702adc1 100644 --- a/InvenTree/templates/InvenTree/settings/category.html +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -2,7 +2,7 @@ {% load i18n %} {% block tabs %} -{% include "InvenTree/settings/tabs.html" with tab='part' %} +{% include "InvenTree/settings/tabs.html" with tab='category' %} {% endblock %} {% block subtitle %} @@ -11,9 +11,16 @@ {% block settings %} -

{% trans "Category Parameter Templates" %}

+
+ {% csrf_token %} + {% load crispy_forms_tags %} + {% crispy form %} +
-
{% trans "Category: XXX (id = 1)" %}
+{% if category %} +
+ +

{% trans "Category Parameter Templates" %}

"; + var bDel = ""; + + var html = value + html += "
" + bEdit + bDel + "
"; + + return html; + } + } ] }); @@ -72,5 +81,29 @@ }); }); + $("#param-table").on('click', '.template-edit', function() { + var button = $(this); + + var url = "/part/category/{{ category }}/parameters/" + button.attr('pk') + "/edit/"; + + launchModalForm(url, { + success: function() { + $("#param-table").bootstrapTable('refresh'); + } + }); + }); + + $("#param-table").on('click', '.template-delete', function() { + var button = $(this); + + var url = "/part/category/{{ category }}/parameters/" + button.attr('pk') + "/delete/"; + + launchModalForm(url, { + success: function() { + $("#param-table").bootstrapTable('refresh'); + } + }); + }); + {% endif %} {% endblock %} From 978b5f869da1ccf7ff836e620846a105e53bf1e3 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 2 Nov 2020 12:20:29 -0500 Subject: [PATCH 06/20] Added checkbox to add parameter template to all categories --- InvenTree/InvenTree/forms.py | 10 +++----- InvenTree/part/forms.py | 5 ++++ InvenTree/part/models.py | 5 +--- InvenTree/part/views.py | 47 +++++++++++++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 707c0c5c79..43454374ac 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -201,7 +201,8 @@ class ColorThemeSelectForm(forms.ModelForm): class SettingCategorySelectForm(forms.ModelForm): """ Form for setting category settings """ - name = forms.ChoiceField(choices=(), required=False) + name = forms.ChoiceField(choices=[('', '-' * 10)] + PartCategory.get_parent_categories(), + required=False) class Meta: model = PartCategory @@ -212,9 +213,6 @@ class SettingCategorySelectForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(SettingCategorySelectForm, self).__init__(*args, **kwargs) - # Populate category choices - self.fields['name'].choices = PartCategory.get_parent_categories() - self.helper = FormHelper() # Form rendering self.helper.form_show_labels = False @@ -222,10 +220,10 @@ class SettingCategorySelectForm(forms.ModelForm): Div( Div(Field('name'), css_class='col-sm-6', - style='width: 200px;'), + style='width: auto;'), Div(StrictButton(_('Select Category'), css_class='btn btn-primary', type='submit'), css_class='col-sm-6', - style='width: auto;'), + style='width: auto; padding-left: 0;'), css_class='row', ), ) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 81a75a22fb..f553229b5c 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -251,12 +251,17 @@ class EditCategoryForm(HelperForm): class EditCategoryParameterTemplateForm(HelperForm): """ Form for editing a PartCategoryParameterTemplate object """ + add_to_all_categories = forms.BooleanField(required=False, + initial=False, + help_text=_('Add parameter template to all categories')) + class Meta: model = PartCategoryParameterTemplate fields = [ 'category', 'parameter_template', 'default_value', + 'add_to_all_categories', ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 37788ae56a..55eef73018 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -167,13 +167,10 @@ class PartCategory(InvenTreeTree): def get_parent_categories(cls): """ Return tuple list of parent (root) categories """ - # Store parent categories (add empty label) - parent_categories = [ - ('', '-' * 10) - ] # Get root nodes root_categories = cls.objects.filter(level=0) + parent_categories = [] for category in root_categories: parent_categories.append((category.id, category.name)) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 09eb8b8a4f..e913bda57d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from django.core.exceptions import ValidationError from django.db import transaction +from django.db.utils import IntegrityError from django.shortcuts import get_object_or_404 from django.shortcuts import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ @@ -2178,7 +2179,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView): # Get category category = self.get_initial()['category'] - # Get existing related parts + # Get existing parameter templates parameters = [template.parameter_template.pk for template in category.get_parameter_templates()] @@ -2188,13 +2189,51 @@ class CategoryParameterTemplateCreate(AjaxCreateView): if (choice[0] not in parameters): updated_choices.append(choice) - # Update choices for related part + # Update choices for parameter templates form.fields['parameter_template'].choices = updated_choices except KeyError: pass return form + def post(self, request, *args, **kwargs): + """ Capture the POST request + + - If the add_to_all_categories object is set, link parameter template to + all categories + """ + + form = self.get_form() + + valid = form.is_valid() + + if valid: + all_categories = form.cleaned_data['add_to_all_categories'] + + if all_categories: + selected_category = int(self.kwargs.get('pk', 0)) + parameter_template = form.cleaned_data['parameter_template'] + default_value = form.cleaned_data['default_value'] + + # Add parameter template and default value to all categories + for category_id, category_name in PartCategory.get_parent_categories(): + # Change category_id type to integer + category_id = int(category_id) + # Skip selected category (will be processed in the post call) + if category_id != selected_category: + # Get category + category = PartCategory.objects.get(pk=category_id) + try: + cat_template = PartCategoryParameterTemplate.objects.create(category=category, + parameter_template=parameter_template, + default_value=default_value) + cat_template.save() + except IntegrityError: + # Parameter template is already linked to category + pass + + return super().post(request, *args, **kwargs) + class CategoryParameterTemplateEdit(AjaxUpdateView): """ View for editing a PartCategoryParameterTemplate """ @@ -2230,7 +2269,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): # Get category category = PartCategory.objects.get(pk=self.kwargs.get('pk', None)) - # Get existing related parts + # Get existing parameter templates parameters = [template.parameter_template.pk for template in category.get_parameter_templates()] @@ -2240,7 +2279,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): if (choice[0] not in parameters): updated_choices.append(choice) - # Update choices for related part + # Update choices for parameter templates form.fields['parameter_template'].choices = updated_choices except KeyError: pass From 34b784d1e47b70627c374e3724e019d93d0f3f1b Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 2 Nov 2020 13:14:31 -0500 Subject: [PATCH 07/20] Added setting, checkbox (PartCreateView only) and hook to create part parameters from category templates --- InvenTree/common/models.py | 7 +++++++ InvenTree/part/forms.py | 6 ++++++ InvenTree/part/views.py | 18 ++++++++++++++++++ .../templates/InvenTree/settings/part.html | 1 + 4 files changed, 32 insertions(+) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 0d3afbe226..9b52a89eba 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -85,6 +85,13 @@ class InvenTreeSetting(models.Model): 'validator': bool }, + 'PART_CATEGORY_PARAMETERS': { + 'name': _('Create Parameters From Category Templates'), + 'description': _('Automatically create part parameters from category templates'), + 'default': False, + 'validator': bool + }, + 'BUILDORDER_REFERENCE_PREFIX': { 'name': _('Build Order Reference Prefix'), 'description': _('Prefix value for build order reference'), diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index f553229b5c..dd41a5b583 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -186,6 +186,11 @@ class EditPartForm(HelperForm): help_text=_('Confirm part creation'), widget=forms.HiddenInput()) + category_templates = forms.BooleanField(required=False, + initial=False, + help_text=_('Create parameters from category templates'), + widget=forms.HiddenInput()) + class Meta: model = Part fields = [ @@ -193,6 +198,7 @@ class EditPartForm(HelperForm): 'parameters_copy', 'confirm_creation', 'category', + 'category_templates', 'name', 'IPN', 'description', diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index e913bda57d..e17f7da7f0 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -555,6 +555,9 @@ class PartCreate(AjaxCreateView): # Hide the default_supplier field (there are no matching supplier parts yet!) form.fields['default_supplier'].widget = HiddenInput() + # Force display of the 'category_templates' widget + form.fields['category_templates'].widget = CheckboxInput() + return form def post(self, request, *args, **kwargs): @@ -607,6 +610,18 @@ class PartCreate(AjaxCreateView): except AttributeError: pass + # Create part parameters + category_templates = form.cleaned_data['category_templates'] + if category_templates: + # Get category parent + category = form.cleaned_data['category'].get_root() + + for template in category.get_parameter_templates(): + PartParameter.create(part=part, + template=template.parameter_template, + data=template.default_value, + save=True) + return self.renderJsonResponse(request, form, data, context=context) def get_initial(self): @@ -630,6 +645,9 @@ class PartCreate(AjaxCreateView): if label in self.request.GET: initials[label] = self.request.GET.get(label) + # Automatically create part parameters from category templates + initials['category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False)) + return initials diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index 19578ba858..495526c94a 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -18,6 +18,7 @@ {% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_TESTS" %} + {% include "InvenTree/settings/setting.html" with key="PART_CATEGORY_PARAMETERS" %} From 43fab8a8b379d0a2a63ebf0114fa5244ec86d2fb Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 2 Nov 2020 13:28:34 -0500 Subject: [PATCH 08/20] Backtracked on setting category choices (fixed failed migration) --- InvenTree/InvenTree/forms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 43454374ac..a0fcd9979d 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -201,8 +201,7 @@ class ColorThemeSelectForm(forms.ModelForm): class SettingCategorySelectForm(forms.ModelForm): """ Form for setting category settings """ - name = forms.ChoiceField(choices=[('', '-' * 10)] + PartCategory.get_parent_categories(), - required=False) + name = forms.ChoiceField(required=False) class Meta: model = PartCategory @@ -213,6 +212,9 @@ class SettingCategorySelectForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(SettingCategorySelectForm, self).__init__(*args, **kwargs) + # Populate category choices + self.fields['name'].choices = [('', '-' * 10)] + PartCategory.get_parent_categories() + self.helper = FormHelper() # Form rendering self.helper.form_show_labels = False From 6320384ecb9efe6b2cf147418500b961bd9d43ee Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 2 Nov 2020 15:05:37 -0500 Subject: [PATCH 09/20] Fixed category parameter template edit form --- InvenTree/part/forms.py | 3 ++- InvenTree/part/views.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index dd41a5b583..94be763b2d 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -188,7 +188,8 @@ class EditPartForm(HelperForm): category_templates = forms.BooleanField(required=False, initial=False, - help_text=_('Create parameters from category templates'), + help_text=_('Create parameters based on default category templates'), + label=_('Copy category parameter templates'), widget=forms.HiddenInput()) class Meta: diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index e17f7da7f0..2450777cd9 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2168,7 +2168,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView): def get_initial(self): """ Get initial data for Category """ - initials = super().get_initial().copy() + initials = super().get_initial() category_id = self.kwargs.get('pk', None) @@ -2194,7 +2194,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView): form.cleaned_data['category'] = self.kwargs.get('pk', None) try: - # Get category + # Get selected category category = self.get_initial()['category'] # Get existing parameter templates @@ -2284,12 +2284,15 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): form.cleaned_data['category'] = self.kwargs.get('pk', None) try: - # Get category + # Get selected category category = PartCategory.objects.get(pk=self.kwargs.get('pk', None)) + # Get selected template + selected_template = self.get_object().parameter_template # Get existing parameter templates parameters = [template.parameter_template.pk - for template in category.get_parameter_templates()] + for template in category.get_parameter_templates() + if template.parameter_template.pk != selected_template.pk] # Exclude templates already linked to category updated_choices = [] @@ -2299,6 +2302,8 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): # Update choices for parameter templates form.fields['parameter_template'].choices = updated_choices + # Set initial choice to current template + form.fields['parameter_template'].initial = selected_template except KeyError: pass From 13a07be7285e381acd6e50323b323acdc73ce0b7 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 2 Nov 2020 15:35:54 -0500 Subject: [PATCH 10/20] Added PartCategoryParameterTemplate tests --- InvenTree/part/fixtures/params.yaml | 19 +++++++++++++++++-- InvenTree/part/test_param.py | 20 +++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/fixtures/params.yaml b/InvenTree/part/fixtures/params.yaml index 121bb79074..2dfcada698 100644 --- a/InvenTree/part/fixtures/params.yaml +++ b/InvenTree/part/fixtures/params.yaml @@ -18,7 +18,7 @@ name: Thickness units: mm -# And some parameters (requires part.yaml) +# Add some parameters to parts (requires part.yaml) - model: part.PartParameter pk: 1 fields: @@ -31,4 +31,19 @@ fields: part: 2 template: 1 - data: 12 \ No newline at end of file + data: 12 + +# Add some template parameters to categories (requires category.yaml) +- model: part.PartCategoryParameterTemplate + pk: 1 + fields: + category: 7 + parameter_template: 1 + default_value: '2.8' + +- model: part.PartCategoryParameterTemplate + pk: 3 + fields: + category: 7 + parameter_template: 3 + default_value: '0.5' diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index 6876a2b5df..2bd3be2478 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -6,7 +6,9 @@ from __future__ import unicode_literals from django.test import TestCase import django.core.exceptions as django_exceptions +from .models import PartCategory from .models import PartParameter, PartParameterTemplate +from .models import PartCategoryParameterTemplate class TestParams(TestCase): @@ -24,7 +26,10 @@ class TestParams(TestCase): self.assertEquals(str(t1), 'Length (mm)') p1 = PartParameter.objects.get(pk=1) - self.assertEqual(str(p1), "M2x4 LPHS : Length = 4mm") + self.assertEqual(str(p1), 'M2x4 LPHS : Length = 4mm') + + c1 = PartCategoryParameterTemplate.objects.get(pk=1) + self.assertEqual(str(c1), 'Mechanical | Length | 2.8') def test_validate(self): @@ -40,3 +45,16 @@ class TestParams(TestCase): t3 = PartParameterTemplate(name='aBcde', units='dd') t3.full_clean() t3.save() + + n = PartCategoryParameterTemplate.objects.all().count() + self.assertEqual(n, 2) + + parent_category = PartCategory.objects.get(pk=8).get_root() + self.assertEqual(parent_category.pk, 7) + + c1 = PartCategoryParameterTemplate(category=parent_category, + parameter_template=t1, + default_value='xyz') + c1.save() + + self.assertEqual(n + 1, PartCategoryParameterTemplate.objects.filter(category=7).count()) From 6b702ef676458b99d997a11b229b23408f2aa114 Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 3 Nov 2020 07:27:51 -0500 Subject: [PATCH 11/20] Manually merged part migrations --- .../migrations/0052_auto_20201027_1557.py | 19 ------------------- InvenTree/part/migrations/0052_partrelated.py | 7 ++++++- .../migrations/0053_merge_20201103_1028.py | 14 -------------- .../0053_partcategoryparametertemplate.py | 2 +- 4 files changed, 7 insertions(+), 35 deletions(-) delete mode 100644 InvenTree/part/migrations/0052_auto_20201027_1557.py delete mode 100644 InvenTree/part/migrations/0053_merge_20201103_1028.py diff --git a/InvenTree/part/migrations/0052_auto_20201027_1557.py b/InvenTree/part/migrations/0052_auto_20201027_1557.py deleted file mode 100644 index 94dbcac06e..0000000000 --- a/InvenTree/part/migrations/0052_auto_20201027_1557.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.0.7 on 2020-10-27 04:57 - -import InvenTree.fields -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('part', '0051_bomitem_optional'), - ] - - operations = [ - migrations.AlterField( - model_name='part', - name='link', - field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True), - ), - ] diff --git a/InvenTree/part/migrations/0052_partrelated.py b/InvenTree/part/migrations/0052_partrelated.py index a8672ba7dc..7f9dc896a5 100644 --- a/InvenTree/part/migrations/0052_partrelated.py +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -2,7 +2,7 @@ from django.db import migrations, models import django.db.models.deletion - +import InvenTree.fields class Migration(migrations.Migration): @@ -19,4 +19,9 @@ class Migration(migrations.Migration): ('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')), ], ), + migrations.AlterField( + model_name='part', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True), + ), ] diff --git a/InvenTree/part/migrations/0053_merge_20201103_1028.py b/InvenTree/part/migrations/0053_merge_20201103_1028.py deleted file mode 100644 index d42595675a..0000000000 --- a/InvenTree/part/migrations/0053_merge_20201103_1028.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.7 on 2020-11-03 10:28 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('part', '0052_auto_20201027_1557'), - ('part', '0052_partrelated'), - ] - - operations = [ - ] diff --git a/InvenTree/part/migrations/0053_partcategoryparametertemplate.py b/InvenTree/part/migrations/0053_partcategoryparametertemplate.py index e9a1e90af7..6f2809af12 100644 --- a/InvenTree/part/migrations/0053_partcategoryparametertemplate.py +++ b/InvenTree/part/migrations/0053_partcategoryparametertemplate.py @@ -7,7 +7,7 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('part', '0052_auto_20201027_1557'), + ('part', '0052_partrelated'), ] operations = [ From 72b5a105f848a2bb1cbfdc914aa396c69536356a Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 3 Nov 2020 14:45:53 -0500 Subject: [PATCH 12/20] Made all categories accessible for parameter templates configuration --- InvenTree/InvenTree/forms.py | 11 ++++------- InvenTree/InvenTree/views.py | 6 ++---- .../templates/InvenTree/settings/category.html | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index a0fcd9979d..3038a1cd7f 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -201,28 +201,25 @@ class ColorThemeSelectForm(forms.ModelForm): class SettingCategorySelectForm(forms.ModelForm): """ Form for setting category settings """ - name = forms.ChoiceField(required=False) + category = forms.ModelChoiceField(queryset=PartCategory.objects.all()) class Meta: model = PartCategory fields = [ - 'name' + 'category' ] def __init__(self, *args, **kwargs): super(SettingCategorySelectForm, self).__init__(*args, **kwargs) - # Populate category choices - self.fields['name'].choices = [('', '-' * 10)] + PartCategory.get_parent_categories() - self.helper = FormHelper() # Form rendering self.helper.form_show_labels = False self.helper.layout = Layout( Div( - Div(Field('name'), + Div(Field('category'), css_class='col-sm-6', - style='width: auto;'), + style='width: 30%;'), Div(StrictButton(_('Select Category'), css_class='btn btn-primary', type='submit'), css_class='col-sm-6', style='width: auto; padding-left: 0;'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index c59b0cdcdd..e61e2d497c 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -765,7 +765,7 @@ class SettingCategorySelectView(FormView): category = self.request.GET.get('category', None) if category: - initial['name'] = category + initial['category'] = category return initial @@ -778,11 +778,9 @@ class SettingCategorySelectView(FormView): form = self.get_form() if form.is_valid(): - category = form.cleaned_data['name'] - context = self.get_context_data() - context['category'] = category + context['category'] = form.cleaned_data['category'] return super(SettingCategorySelectView, self).render_to_response(context) diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html index d5e27d79a7..d17efd9c9a 100644 --- a/InvenTree/templates/InvenTree/settings/category.html +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -14,7 +14,9 @@
{% csrf_token %} {% load crispy_forms_tags %} - {% crispy form %} +
+ {% crispy form %} +
{% if category %} @@ -35,10 +37,14 @@ {% block js_ready %} {{ block.super }} -{% if category %} + $(document).ready(function() { + attachSelect('#category-select'); + }); + +{% if category %} $("#param-table").inventreeTable({ - url: "{% url 'api-part-category-parameters' category %}", + url: "{% url 'api-part-category-parameters' category.pk %}", queryParams: { ordering: 'name', }, @@ -74,7 +80,7 @@ $("#new-param").click(function() { - launchModalForm("{% url 'category-param-template-create' category %}", { + launchModalForm("{% url 'category-param-template-create' category.pk %}", { success: function() { $("#param-table").bootstrapTable('refresh'); }, @@ -84,7 +90,7 @@ $("#param-table").on('click', '.template-edit', function() { var button = $(this); - var url = "/part/category/{{ category }}/parameters/" + button.attr('pk') + "/edit/"; + var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/edit/"; launchModalForm(url, { success: function() { @@ -96,7 +102,7 @@ $("#param-table").on('click', '.template-delete', function() { var button = $(this); - var url = "/part/category/{{ category }}/parameters/" + button.attr('pk') + "/delete/"; + var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/delete/"; launchModalForm(url, { success: function() { From 5a5a36083e1b2be47f3f814d7155178df8c953ad Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 3 Nov 2020 16:54:46 -0500 Subject: [PATCH 13/20] Finalized implementation when creating new part --- InvenTree/part/forms.py | 17 ++++++++---- InvenTree/part/test_param.py | 5 ++-- InvenTree/part/views.py | 54 +++++++++++++++++++++++++----------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 4e6413b34f..47b7ac21bc 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -205,11 +205,15 @@ class EditPartForm(HelperForm): help_text=_('Confirm part creation'), widget=forms.HiddenInput()) - category_templates = forms.BooleanField(required=False, - initial=False, - help_text=_('Create parameters based on default category templates'), - label=_('Copy category parameter templates'), - widget=forms.HiddenInput()) + selected_category_templates = forms.BooleanField(required=False, + initial=False, + label=_('Include selected category parameter templates'), + widget=forms.HiddenInput()) + + parent_category_templates = forms.BooleanField(required=False, + initial=False, + label=_('Include parent category parameter templates'), + widget=forms.HiddenInput()) class Meta: model = Part @@ -218,7 +222,8 @@ class EditPartForm(HelperForm): 'parameters_copy', 'confirm_creation', 'category', - 'category_templates', + 'selected_category_templates', + 'parent_category_templates', 'name', 'IPN', 'description', diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index 2bd3be2478..ad0f5ed3b6 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -49,10 +49,9 @@ class TestParams(TestCase): n = PartCategoryParameterTemplate.objects.all().count() self.assertEqual(n, 2) - parent_category = PartCategory.objects.get(pk=8).get_root() - self.assertEqual(parent_category.pk, 7) + category = PartCategory.objects.get(pk=7) - c1 = PartCategoryParameterTemplate(category=parent_category, + c1 = PartCategoryParameterTemplate(category=category, parameter_template=t1, default_value='xyz') c1.save() diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 099dd3c515..857cf94106 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -638,8 +638,9 @@ class PartCreate(AjaxCreateView): # Hide the default_supplier field (there are no matching supplier parts yet!) form.fields['default_supplier'].widget = HiddenInput() - # Force display of the 'category_templates' widget - form.fields['category_templates'].widget = CheckboxInput() + # Display category templates widgets + form.fields['selected_category_templates'].widget = CheckboxInput() + form.fields['parent_category_templates'].widget = CheckboxInput() return form @@ -693,17 +694,40 @@ class PartCreate(AjaxCreateView): except AttributeError: pass - # Create part parameters - category_templates = form.cleaned_data['category_templates'] + # Store templates added to part + template_list = [] + + # Create part parameters for selected category + category_templates = form.cleaned_data['selected_category_templates'] if category_templates: - # Get category parent + # Get selected category + category = form.cleaned_data['category'] + + for template in category.get_parameter_templates(): + parameter = PartParameter.create(part=part, + template=template.parameter_template, + data=template.default_value, + save=True) + if parameter: + template_list.append(template.parameter_template) + + # Create part parameters for parent category + category_templates = form.cleaned_data['parent_category_templates'] + if category_templates: + # Get parent category category = form.cleaned_data['category'].get_root() for template in category.get_parameter_templates(): - PartParameter.create(part=part, - template=template.parameter_template, - data=template.default_value, - save=True) + # Check that template wasn't already added + if template.parameter_template not in template_list: + try: + PartParameter.create(part=part, + template=template.parameter_template, + data=template.default_value, + save=True) + except IntegrityError: + # PartParameter already exists + pass return self.renderJsonResponse(request, form, data, context=context) @@ -729,7 +753,8 @@ class PartCreate(AjaxCreateView): initials[label] = self.request.GET.get(label) # Automatically create part parameters from category templates - initials['category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False)) + initials['selected_category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False)) + initials['parent_category_templates'] = initials['selected_category_templates'] return initials @@ -2323,13 +2348,9 @@ class CategoryParameterTemplateCreate(AjaxCreateView): default_value = form.cleaned_data['default_value'] # Add parameter template and default value to all categories - for category_id, category_name in PartCategory.get_parent_categories(): - # Change category_id type to integer - category_id = int(category_id) + for category in PartCategory.objects.all(): # Skip selected category (will be processed in the post call) - if category_id != selected_category: - # Get category - category = PartCategory.objects.get(pk=category_id) + if category.pk != selected_category: try: cat_template = PartCategoryParameterTemplate.objects.create(category=category, parameter_template=parameter_template, @@ -2368,6 +2389,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): form = super(AjaxUpdateView, self).get_form() form.fields['category'].widget = HiddenInput() + form.fields['add_to_all_categories'].widget = HiddenInput() if form.is_valid(): form.cleaned_data['category'] = self.kwargs.get('pk', None) From 279d5a00cedd84eedc37746df0e48095376e9eb4 Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 3 Nov 2020 16:58:53 -0500 Subject: [PATCH 14/20] Switched to get_ancestors to transverse all parents categories (not only root) --- InvenTree/part/views.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 857cf94106..25d2eb8bfb 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -714,20 +714,21 @@ class PartCreate(AjaxCreateView): # Create part parameters for parent category category_templates = form.cleaned_data['parent_category_templates'] if category_templates: - # Get parent category - category = form.cleaned_data['category'].get_root() + # Get parent categories + parent_categories = form.cleaned_data['category'].get_ancestors() - for template in category.get_parameter_templates(): - # Check that template wasn't already added - if template.parameter_template not in template_list: - try: - PartParameter.create(part=part, - template=template.parameter_template, - data=template.default_value, - save=True) - except IntegrityError: - # PartParameter already exists - pass + for category in parent_categories: + for template in category.get_parameter_templates(): + # Check that template wasn't already added + if template.parameter_template not in template_list: + try: + PartParameter.create(part=part, + template=template.parameter_template, + data=template.default_value, + save=True) + except IntegrityError: + # PartParameter already exists + pass return self.renderJsonResponse(request, form, data, context=context) From 4e157fe9561a8fa5171f5e73c451e03b695a514a Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 3 Nov 2020 17:05:08 -0500 Subject: [PATCH 15/20] Fixed text for parent categories checkbox --- InvenTree/part/forms.py | 2 +- InvenTree/part/test_param.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 47b7ac21bc..cb4691e05b 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -212,7 +212,7 @@ class EditPartForm(HelperForm): parent_category_templates = forms.BooleanField(required=False, initial=False, - label=_('Include parent category parameter templates'), + label=_('Include parent categories parameter templates'), widget=forms.HiddenInput()) class Meta: diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index ad0f5ed3b6..66b2bf303c 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -46,6 +46,7 @@ class TestParams(TestCase): t3.full_clean() t3.save() + # Category templates n = PartCategoryParameterTemplate.objects.all().count() self.assertEqual(n, 2) From 1c14c2237ac43e84e41d1f617dbf20cab1acbb50 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 4 Nov 2020 09:52:26 -0500 Subject: [PATCH 16/20] Moved category templates processing to Part save() method --- InvenTree/InvenTree/forms.py | 4 +- InvenTree/common/models.py | 6 +-- InvenTree/part/models.py | 42 +++++++++++++++++ InvenTree/part/views.py | 45 ++++--------------- .../InvenTree/settings/category.html | 5 +-- 5 files changed, 57 insertions(+), 45 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 3038a1cd7f..3a91821a19 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -219,10 +219,10 @@ class SettingCategorySelectForm(forms.ModelForm): Div( Div(Field('category'), css_class='col-sm-6', - style='width: 30%;'), + style='width: 70%;'), Div(StrictButton(_('Select Category'), css_class='btn btn-primary', type='submit'), css_class='col-sm-6', - style='width: auto; padding-left: 0;'), + style='width: 30%; padding-left: 0;'), css_class='row', ), ) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 9b52a89eba..e5bc10c906 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -86,9 +86,9 @@ class InvenTreeSetting(models.Model): }, 'PART_CATEGORY_PARAMETERS': { - 'name': _('Create Parameters From Category Templates'), - 'description': _('Automatically create part parameters from category templates'), - 'default': False, + 'name': _('Copy Category Parameter Templates'), + 'description': _('Copy category parameter templates when creating a part'), + 'default': True, 'validator': bool }, diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 705e4c6afd..633915466f 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -12,6 +12,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction +from django.db.utils import IntegrityError from django.db.models import Sum, UniqueConstraint from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator @@ -324,6 +325,9 @@ class Part(MPTTModel): If not, it is considered "orphaned" and will be deleted. """ + # Get category templates settings + add_category_templates = kwargs.pop('add_category_templates', None) + if self.pk: previous = Part.objects.get(pk=self.pk) @@ -339,6 +343,44 @@ class Part(MPTTModel): super().save(*args, **kwargs) + if add_category_templates: + # Get part category + category = self.category + + if add_category_templates: + # Store templates added to part + template_list = [] + + # Create part parameters for selected category + category_templates = add_category_templates['main'] + if category_templates: + for template in category.get_parameter_templates(): + parameter = PartParameter.create(part=self, + template=template.parameter_template, + data=template.default_value, + save=True) + if parameter: + template_list.append(template.parameter_template) + + # Create part parameters for parent category + category_templates = add_category_templates['parent'] + if category_templates: + # Get parent categories + parent_categories = category.get_ancestors() + + for category in parent_categories: + for template in category.get_parameter_templates(): + # Check that template wasn't already added + if template.parameter_template not in template_list: + try: + PartParameter.create(part=self, + template=template.parameter_template, + data=template.default_value, + save=True) + except IntegrityError: + # PartParameter already exists + pass + def __str__(self): return f"{self.full_name} - {self.description}" diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 25d2eb8bfb..65b74ec7b2 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -684,7 +684,14 @@ class PartCreate(AjaxCreateView): # Record the user who created this part part.creation_user = request.user - part.save() + # Store category templates settings + add_category_templates = { + 'main': form.cleaned_data['selected_category_templates'], + 'parent': form.cleaned_data['parent_category_templates'], + } + + # Save part and pass category template settings + part.save(**{'add_category_templates': add_category_templates}) data['pk'] = part.pk data['text'] = str(part) @@ -694,42 +701,6 @@ class PartCreate(AjaxCreateView): except AttributeError: pass - # Store templates added to part - template_list = [] - - # Create part parameters for selected category - category_templates = form.cleaned_data['selected_category_templates'] - if category_templates: - # Get selected category - category = form.cleaned_data['category'] - - for template in category.get_parameter_templates(): - parameter = PartParameter.create(part=part, - template=template.parameter_template, - data=template.default_value, - save=True) - if parameter: - template_list.append(template.parameter_template) - - # Create part parameters for parent category - category_templates = form.cleaned_data['parent_category_templates'] - if category_templates: - # Get parent categories - parent_categories = form.cleaned_data['category'].get_ancestors() - - for category in parent_categories: - for template in category.get_parameter_templates(): - # Check that template wasn't already added - if template.parameter_template not in template_list: - try: - PartParameter.create(part=part, - template=template.parameter_template, - data=template.default_value, - save=True) - except IntegrityError: - # PartParameter already exists - pass - return self.renderJsonResponse(request, form, data, context=context) def get_initial(self): diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html index d17efd9c9a..c84573e8b4 100644 --- a/InvenTree/templates/InvenTree/settings/category.html +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -37,7 +37,8 @@ {% block js_ready %} {{ block.super }} - + + {# Convert dropdown to select2 format #} $(document).ready(function() { attachSelect('#category-select'); }); @@ -78,7 +79,6 @@ ] }); - $("#new-param").click(function() { launchModalForm("{% url 'category-param-template-create' category.pk %}", { success: function() { @@ -110,6 +110,5 @@ } }); }); - {% endif %} {% endblock %} From e401bb8e3c5e27de81a33d61de0e95e2d9bc9971 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 4 Nov 2020 12:06:07 -0500 Subject: [PATCH 17/20] Improved tests, fixed admin, improved naming --- InvenTree/part/admin.py | 9 +------ InvenTree/part/fixtures/params.yaml | 2 +- InvenTree/part/forms.py | 2 +- InvenTree/part/models.py | 5 +++- InvenTree/part/test_param.py | 39 ++++++++++++++++++++++++++--- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 02e6974aef..1d1c981b74 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -277,14 +277,7 @@ class ParameterAdmin(ImportExportModelAdmin): class PartCategoryParameterAdmin(admin.ModelAdmin): - def get_form(self, request, obj=None, **kwargs): - """ Display only parent categories as choices for category field """ - - form = super().get_form(request, obj, **kwargs) - - form.base_fields['category'].choices = PartCategory.get_parent_categories() - - return form + pass class PartSellPriceBreakAdmin(admin.ModelAdmin): diff --git a/InvenTree/part/fixtures/params.yaml b/InvenTree/part/fixtures/params.yaml index 2dfcada698..e65c7335cc 100644 --- a/InvenTree/part/fixtures/params.yaml +++ b/InvenTree/part/fixtures/params.yaml @@ -42,7 +42,7 @@ default_value: '2.8' - model: part.PartCategoryParameterTemplate - pk: 3 + pk: 2 fields: category: 7 parameter_template: 3 diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index cb4691e05b..20507bd392 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -207,7 +207,7 @@ class EditPartForm(HelperForm): selected_category_templates = forms.BooleanField(required=False, initial=False, - label=_('Include selected category parameter templates'), + label=_('Include category parameter templates'), widget=forms.HiddenInput()) parent_category_templates = forms.BooleanField(required=False, diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 633915466f..6ebc6fe804 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -180,7 +180,9 @@ class PartCategory(InvenTreeTree): def get_parameter_templates(self): """ Return parameter templates associated to category """ - return PartCategoryParameterTemplate.objects.filter(category=self.id) + prefetch = PartCategoryParameterTemplate.objects.prefetch_related('category', 'parameter_template') + + return prefetch.filter(category=self.id) @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') @@ -1701,6 +1703,7 @@ class PartCategoryParameterTemplate(models.Model): def __str__(self): """ String representation of a PartCategoryParameterTemplate (admin interface) """ + if self.default_value: return f'{self.category.name} | {self.parameter_template.name} | {self.default_value}' else: diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index 66b2bf303c..24eee44d89 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -3,10 +3,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.test import TestCase +from django.test import TestCase, TransactionTestCase import django.core.exceptions as django_exceptions -from .models import PartCategory +from .models import Part, PartCategory from .models import PartParameter, PartParameterTemplate from .models import PartCategoryParameterTemplate @@ -46,15 +46,46 @@ class TestParams(TestCase): t3.full_clean() t3.save() + +class TestCategoryTemplates(TransactionTestCase): + + fixtures = [ + 'location', + 'category', + 'part', + 'params' + ] + + def test_validate(self): + # Category templates n = PartCategoryParameterTemplate.objects.all().count() self.assertEqual(n, 2) - category = PartCategory.objects.get(pk=7) + category = PartCategory.objects.get(pk=8) + t1 = PartParameterTemplate.objects.get(pk=2) c1 = PartCategoryParameterTemplate(category=category, parameter_template=t1, default_value='xyz') c1.save() - self.assertEqual(n + 1, PartCategoryParameterTemplate.objects.filter(category=7).count()) + n = PartCategoryParameterTemplate.objects.all().count() + self.assertEqual(n, 3) + + # Get test part + part = Part.objects.get(pk=1) + + # Get part parameters count + n_param = part.get_parameters().count() + + add_category_templates = { + 'main': True, + 'parent': True, + } + # Save it with category parameters + part.save(**{'add_category_templates': add_category_templates}) + + # Check new part parameters count + # Only 2 parameters should be added as one already existed with same template + self.assertEqual(n_param + 2, part.get_parameters().count()) From 324645b67c58c81d8288a3fefbb9efa7eb0b6150 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 4 Nov 2020 12:26:10 -0500 Subject: [PATCH 18/20] Added same level category checkbox and method when adding category template --- InvenTree/part/forms.py | 7 ++++++- InvenTree/part/views.py | 28 ++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 20507bd392..63d884a15e 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -282,16 +282,21 @@ class EditCategoryForm(HelperForm): class EditCategoryParameterTemplateForm(HelperForm): """ Form for editing a PartCategoryParameterTemplate object """ + add_to_same_level_categories = forms.BooleanField(required=False, + initial=False, + help_text=_('Add parameter template to same level categories')) + add_to_all_categories = forms.BooleanField(required=False, initial=False, help_text=_('Add parameter template to all categories')) - + class Meta: model = PartCategoryParameterTemplate fields = [ 'category', 'parameter_template', 'default_value', + 'add_to_same_level_categories', 'add_to_all_categories', ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 65b74ec7b2..b522a66159 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2305,6 +2305,8 @@ class CategoryParameterTemplateCreate(AjaxCreateView): - If the add_to_all_categories object is set, link parameter template to all categories + - If the add_to_same_level_categories object is set, link parameter template to + same level categories """ form = self.get_form() @@ -2312,17 +2314,26 @@ class CategoryParameterTemplateCreate(AjaxCreateView): valid = form.is_valid() if valid: - all_categories = form.cleaned_data['add_to_all_categories'] + add_to_same_level_categories = form.cleaned_data['add_to_same_level_categories'] + add_to_all_categories = form.cleaned_data['add_to_all_categories'] - if all_categories: - selected_category = int(self.kwargs.get('pk', 0)) - parameter_template = form.cleaned_data['parameter_template'] - default_value = form.cleaned_data['default_value'] + selected_category = PartCategory.objects.get(pk=int(self.kwargs['pk'])) + parameter_template = form.cleaned_data['parameter_template'] + default_value = form.cleaned_data['default_value'] - # Add parameter template and default value to all categories - for category in PartCategory.objects.all(): + categories = PartCategory.objects.all() + + if add_to_same_level_categories and not add_to_all_categories: + # Get level + level = selected_category.level + # Filter same level categories + categories = categories.filter(level=level) + + if add_to_same_level_categories or add_to_all_categories: + # Add parameter template and default value to categories + for category in categories: # Skip selected category (will be processed in the post call) - if category.pk != selected_category: + if category.pk != selected_category.pk: try: cat_template = PartCategoryParameterTemplate.objects.create(category=category, parameter_template=parameter_template, @@ -2362,6 +2373,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): form.fields['category'].widget = HiddenInput() form.fields['add_to_all_categories'].widget = HiddenInput() + form.fields['add_to_same_level_categories'].widget = HiddenInput() if form.is_valid(): form.cleaned_data['category'] = self.kwargs.get('pk', None) From bfdda847c449fa09a87ed3ef37946fe6bbe957c9 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 11 Nov 2020 11:18:10 -0500 Subject: [PATCH 19/20] Updated part migration reference in 0054 --- InvenTree/part/migrations/0054_auto_20201109_1246.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/migrations/0054_auto_20201109_1246.py b/InvenTree/part/migrations/0054_auto_20201109_1246.py index 705ef51466..830525763e 100644 --- a/InvenTree/part/migrations/0054_auto_20201109_1246.py +++ b/InvenTree/part/migrations/0054_auto_20201109_1246.py @@ -7,7 +7,7 @@ import part.settings class Migration(migrations.Migration): dependencies = [ - ('part', '0053_merge_20201103_1028'), + ('part', '0053_partcategoryparametertemplate'), ] operations = [ From b4fa56fd9644116611af8ef54135383c3031804a Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 11 Nov 2020 12:40:03 -0500 Subject: [PATCH 20/20] Fixed PART_CATEGORY_PARAMETERS duplicate (bad merging... oopsy) --- InvenTree/common/models.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 46e4a5a94a..2173df0ae7 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -92,13 +92,6 @@ class InvenTreeSetting(models.Model): 'validator': bool }, - 'PART_CATEGORY_PARAMETERS': { - 'name': _('Copy Category Parameter Templates'), - 'description': _('Copy category parameter templates when creating a part'), - 'default': True, - 'validator': bool - }, - 'PART_CATEGORY_PARAMETERS': { 'name': _('Copy Category Parameter Templates'), 'description': _('Copy category parameter templates when creating a part'),