Merge pull request #1096 from eeintech/categories_parameters

Categories parameter templates
This commit is contained in:
Oliver 2020-11-12 09:32:01 +11:00 committed by GitHub
commit 643589b4a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 704 additions and 44 deletions

View File

@ -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):
@ -200,3 +201,33 @@ class ColorThemeSelectForm(forms.ModelForm):
css_class='row',
),
)
class SettingCategorySelectForm(forms.ModelForm):
""" Form for setting category settings """
category = forms.ModelChoiceField(queryset=PartCategory.objects.all())
class Meta:
model = PartCategory
fields = [
'category'
]
def __init__(self, *args, **kwargs):
super(SettingCategorySelectForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
# Form rendering
self.helper.form_show_labels = False
self.helper.layout = Layout(
Div(
Div(Field('category'),
css_class='col-sm-6',
style='width: 70%;'),
Div(StrictButton(_('Select Category'), css_class='btn btn-primary', type='submit'),
css_class='col-sm-6',
style='width: 30%; padding-left: 0;'),
css_class='row',
),
)

View File

@ -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,6 +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/?', 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'),

View File

@ -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
@ -750,6 +751,42 @@ 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['category'] = 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():
context = self.get_context_data()
context['category'] = form.cleaned_data['category']
return super(SettingCategorySelectView, self).render_to_response(context)
return self.form_invalid(form)
class DatabaseStatsView(AjaxView):
""" View for displaying database statistics """

View File

@ -92,6 +92,13 @@ 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_COMPONENT': {
'name': _('Component'),
'description': _('Parts can be used as sub-components by default'),

View File

@ -12,6 +12,7 @@ from .models import PartCategory, Part
from .models import PartAttachment, PartStar, PartRelated
from .models import BomItem
from .models import PartParameterTemplate, PartParameter
from .models import PartCategoryParameterTemplate
from .models import PartTestTemplate
from .models import PartSellPriceBreak
@ -274,6 +275,11 @@ class ParameterAdmin(ImportExportModelAdmin):
list_display = ('part', 'template', 'data')
class PartCategoryParameterAdmin(admin.ModelAdmin):
pass
class PartSellPriceBreakAdmin(admin.ModelAdmin):
class Meta:
@ -290,5 +296,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)

View File

@ -21,6 +21,7 @@ from .models import Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment, PartTestTemplate
from .models import PartSellPriceBreak
from .models import PartCategoryParameterTemplate
from build.models import Build
@ -111,6 +112,36 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartCategory.objects.all()
class CategoryParameters(generics.ListAPIView):
""" API endpoint for accessing a list of PartCategory objects.
- GET: Return a list of PartCategory objects
"""
queryset = PartCategoryParameterTemplate.objects.all()
serializer_class = part_serializers.CategoryParameterTemplateSerializer
def get_queryset(self):
"""
Custom filtering:
- Allow filtering by "null" parent to retrieve top-level part categories
"""
cat_id = self.kwargs.get('pk', None)
queryset = super().get_queryset()
if cat_id is not None:
try:
cat_id = int(cat_id)
queryset = queryset.filter(category=cat_id)
except ValueError:
pass
return queryset
class PartSalePriceList(generics.ListCreateAPIView):
"""
API endpoint for list view of PartSalePriceBreak model
@ -864,6 +895,7 @@ part_api_urls = [
# Base URL for PartCategory API endpoints
url(r'^category/', include([
url(r'^(?P<pk>\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
])),

View File

@ -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:
@ -32,3 +32,18 @@
part: 2
template: 1
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: 2
fields:
category: 7
parameter_template: 3
default_value: '0.5'

View File

@ -16,6 +16,7 @@ from django.utils.translation import ugettext as _
from .models import Part, PartCategory, PartAttachment, PartRelated
from .models import BomItem
from .models import PartParameterTemplate, PartParameter
from .models import PartCategoryParameterTemplate
from .models import PartTestTemplate
from .models import PartSellPriceBreak
@ -201,10 +202,22 @@ class EditPartForm(HelperForm):
help_text=_('Confirm part creation'),
widget=forms.HiddenInput())
selected_category_templates = forms.BooleanField(required=False,
initial=False,
label=_('Include category parameter templates'),
widget=forms.HiddenInput())
parent_category_templates = forms.BooleanField(required=False,
initial=False,
label=_('Include parent categories parameter templates'),
widget=forms.HiddenInput())
class Meta:
model = Part
fields = [
'category',
'selected_category_templates',
'parent_category_templates',
'name',
'IPN',
'description',
@ -266,6 +279,28 @@ 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',
]
class EditBomItemForm(HelperForm):
""" Form for editing a BomItem object """

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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 = [
]

View File

@ -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_partrelated'),
]
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'),
),
]

View File

@ -7,7 +7,7 @@ import part.settings
class Migration(migrations.Migration):
dependencies = [
('part', '0053_merge_20201103_1028'),
('part', '0053_partcategoryparametertemplate'),
]
operations = [

View File

@ -12,7 +12,8 @@ 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.utils import IntegrityError
from django.db.models import Sum, UniqueConstraint
from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator
@ -164,6 +165,26 @@ class PartCategory(InvenTreeTree):
return category_parameters
@classmethod
def get_parent_categories(cls):
""" Return tuple list of parent (root) categories """
# Get root nodes
root_categories = cls.objects.filter(level=0)
parent_categories = []
for category in root_categories:
parent_categories.append((category.id, category.name))
return parent_categories
def get_parameter_templates(self):
""" Return parameter templates associated to category """
prefetch = PartCategoryParameterTemplate.objects.prefetch_related('category', 'parameter_template')
return prefetch.filter(category=self.id)
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
def before_delete_part_category(sender, instance, using, **kwargs):
@ -307,6 +328,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)
@ -322,6 +346,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}"
@ -1664,6 +1726,49 @@ 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

View File

@ -15,7 +15,7 @@ from stock.models import StockItem
from .models import (BomItem, Part, PartAttachment, PartCategory,
PartParameter, PartParameterTemplate, PartSellPriceBreak,
PartStar, PartTestTemplate)
PartStar, PartTestTemplate, PartCategoryParameterTemplate)
class CategorySerializer(InvenTreeModelSerializer):
@ -425,3 +425,21 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
'name',
'units',
]
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
""" Serializer for PartCategoryParameterTemplate """
parameter_template_detail = PartParameterTemplateSerializer(source='parameter_template',
many=False,
read_only=True)
class Meta:
model = PartCategoryParameterTemplate
fields = [
'pk',
'category',
'parameter_template',
'parameter_template_detail',
'default_value',
]

View File

@ -3,10 +3,12 @@
# -*- 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 Part, 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,47 @@ class TestParams(TestCase):
t3 = PartParameterTemplate(name='aBcde', units='dd')
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=8)
t1 = PartParameterTemplate.objects.get(pk=2)
c1 = PartCategoryParameterTemplate(category=category,
parameter_template=t1,
default_value='xyz')
c1.save()
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())

View File

@ -80,10 +80,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<pid>\d+)/edit/', views.CategoryParameterTemplateEdit.as_view(), name='category-param-template-edit'),
url(r'^(?P<pid>\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'),
]
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'^parameters/', include(category_parameter_urls)),
url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
]

View File

@ -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 _
@ -23,6 +24,7 @@ from decimal import Decimal, InvalidOperation
from .models import PartCategory, Part, PartAttachment, PartRelated
from .models import PartParameterTemplate, PartParameter
from .models import PartCategoryParameterTemplate
from .models import BomItem
from .models import match_part_names
from .models import PartTestTemplate
@ -625,6 +627,10 @@ class PartCreate(AjaxCreateView):
# Hide the default_supplier field (there are no matching supplier parts yet!)
form.fields['default_supplier'].widget = HiddenInput()
# Display category templates widgets
form.fields['selected_category_templates'].widget = CheckboxInput()
form.fields['parent_category_templates'].widget = CheckboxInput()
return form
def post(self, request, *args, **kwargs):
@ -667,7 +673,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)
@ -700,6 +713,10 @@ class PartCreate(AjaxCreateView):
if label in self.request.GET:
initials[label] = self.request.GET.get(label)
# Automatically create part parameters from category templates
initials['selected_category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False))
initials['parent_category_templates'] = initials['selected_category_templates']
return initials
@ -2215,6 +2232,185 @@ 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()
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)
try:
# Get selected category
category = self.get_initial()['category']
# Get existing parameter templates
parameters = [template.parameter_template.pk
for template in category.get_parameter_templates()]
# Exclude templates already linked to category
updated_choices = []
for choice in form.fields["parameter_template"].choices:
if (choice[0] not in parameters):
updated_choices.append(choice)
# 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
- If the add_to_same_level_categories object is set, link parameter template to
same level categories
"""
form = self.get_form()
valid = form.is_valid()
if valid:
add_to_same_level_categories = form.cleaned_data['add_to_same_level_categories']
add_to_all_categories = form.cleaned_data['add_to_all_categories']
selected_category = PartCategory.objects.get(pk=int(self.kwargs['pk']))
parameter_template = form.cleaned_data['parameter_template']
default_value = form.cleaned_data['default_value']
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.pk:
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 """
role_required = 'part.change'
model = PartCategoryParameterTemplate
form_class = part_forms.EditCategoryParameterTemplateForm
ajax_form_title = _('Edit Category Parameter Template')
def get_object(self):
try:
self.object = self.model.objects.get(pk=self.kwargs['pid'])
except:
return None
return self.object
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(AjaxUpdateView, self).get_form()
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)
try:
# 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()
if template.parameter_template.pk != selected_template.pk]
# Exclude templates already linked to category
updated_choices = []
for choice in form.fields["parameter_template"].choices:
if (choice[0] not in parameters):
updated_choices.append(choice)
# 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
return form
class CategoryParameterTemplateDelete(AjaxDeleteView):
""" View for deleting an existing PartCategoryParameterTemplate """
role_required = 'part.delete'
model = PartCategoryParameterTemplate
ajax_form_title = _("Delete Category Parameter Template")
def get_object(self):
try:
self.object = self.model.objects.get(pk=self.kwargs['pid'])
except:
return None
return self.object
class BomItemDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for BomItem """
context_object_name = 'item'

View File

@ -0,0 +1,114 @@
{% extends "InvenTree/settings/settings.html" %}
{% load i18n %}
{% block tabs %}
{% include "InvenTree/settings/tabs.html" with tab='category' %}
{% endblock %}
{% block subtitle %}
{% trans "Category Settings" %}
{% endblock %}
{% block settings %}
<form action="{% url 'settings-category' %}" method="post">
{% csrf_token %}
{% load crispy_forms_tags %}
<div id="category-select">
{% crispy form %}
</div>
</form>
{% if category %}
<hr>
<h4>{% trans "Category Parameter Templates" %}</h4>
<div id='param-buttons'>
<button class='btn btn-success' id='new-param'>
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
</button>
</div>
<table class='table table-striped table-condensed' id='param-table' data-toolbar='#param-buttons'>
</table>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{# Convert dropdown to select2 format #}
$(document).ready(function() {
attachSelect('#category-select');
});
{% if category %}
$("#param-table").inventreeTable({
url: "{% url 'api-part-category-parameters' category.pk %}",
queryParams: {
ordering: 'name',
},
formatNoMatches: function() { return '{% trans "No category parameter templates found" %}'; },
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'parameter_template_detail.name',
title: '{% trans "Parameter Template" %}',
sortable: 'true',
},
{
field: 'default_value',
title: '{% trans "Default Value" %}',
sortable: 'true',
formatter: function(value, row, index, field) {
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
var html = value
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
return html;
}
}
]
});
$("#new-param").click(function() {
launchModalForm("{% url 'category-param-template-create' category.pk %}", {
success: function() {
$("#param-table").bootstrapTable('refresh');
},
});
});
$("#param-table").on('click', '.template-edit', function() {
var button = $(this);
var url = "/part/category/{{ category.pk }}/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.pk }}/parameters/" + button.attr('pk') + "/delete/";
launchModalForm(url, {
success: function() {
$("#param-table").bootstrapTable('refresh');
}
});
});
{% endif %}
{% endblock %}

View File

@ -27,6 +27,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" %}
</tbody>
</table>

View File

@ -18,6 +18,9 @@
<li{% ifequal tab 'currency' %} class='active'{% endifequal %}>
<a href="{% url 'settings-currency' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currency" %}</a>
</li>
<li{% ifequal tab 'category' %} class='active'{% endifequal %}>
<a href="{% url 'settings-category' %}"><span class='fa fa-sitemap'></span> {% trans "Categories" %}</a>
</li>
<li{% ifequal tab 'part' %} class='active'{% endifequal %}>
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> {% trans "Parts" %}</a>
</li>

View File

@ -58,6 +58,7 @@ class RuleSet(models.Model):
'part_partparametertemplate',
'part_partparameter',
'part_partrelated',
'part_partcategoryparametertemplate',
],
'stock': [
'stock_stockitem',