diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 136d486b0b..e7489c9810 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -4,6 +4,7 @@ from import_export.admin import ImportExportModelAdmin from .models import PartCategory, Part from .models import PartAttachment, PartStar from .models import BomItem +from .models import PartParameterTemplate, PartParameter class PartAdmin(ImportExportModelAdmin): @@ -30,17 +31,18 @@ class BomItemAdmin(ImportExportModelAdmin): list_display = ('part', 'sub_part', 'quantity') -""" -class ParameterTemplateAdmin(admin.ModelAdmin): - list_display = ('name', 'units', 'format') +class ParameterTemplateAdmin(ImportExportModelAdmin): + list_display = ('name', 'units') -class ParameterAdmin(admin.ModelAdmin): - list_display = ('part', 'template', 'value') -""" +class ParameterAdmin(ImportExportModelAdmin): + list_display = ('part', 'template', 'data') + admin.site.register(Part, PartAdmin) admin.site.register(PartCategory, PartCategoryAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartStar, PartStarAdmin) admin.site.register(BomItem, BomItemAdmin) +admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) +admin.site.register(PartParameter, ParameterAdmin) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 0d82183a93..bbb136b28d 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -11,6 +11,7 @@ from django import forms from .models import Part, PartCategory, PartAttachment from .models import BomItem +from .models import PartParameterTemplate, PartParameter class PartImageForm(HelperForm): @@ -98,6 +99,29 @@ class EditPartForm(HelperForm): ] +class EditPartParameterTemplateForm(HelperForm): + """ Form for editing a PartParameterTemplate object """ + + class Meta: + model = PartParameterTemplate + fields = [ + 'name', + 'units' + ] + + +class EditPartParameterForm(HelperForm): + """ Form for editing a PartParameter object """ + + class Meta: + model = PartParameter + fields = [ + 'part', + 'template', + 'data' + ] + + class EditCategoryForm(HelperForm): """ Form for editing a PartCategory object """ diff --git a/InvenTree/part/migrations/0014_partparameter.py b/InvenTree/part/migrations/0014_partparameter.py new file mode 100644 index 0000000000..a1eef38ec6 --- /dev/null +++ b/InvenTree/part/migrations/0014_partparameter.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-08-20 02:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0013_auto_20190628_0951'), + ] + + operations = [ + migrations.CreateModel( + name='PartParameter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Parameter Name', max_length=100)), + ('data', models.CharField(help_text='Parameter Value', max_length=100)), + ('part', models.ForeignKey(help_text='Parent Part', on_delete=django.db.models.deletion.CASCADE, related_name='parameters', to='part.Part')), + ], + ), + ] diff --git a/InvenTree/part/migrations/0015_auto_20190820_0251.py b/InvenTree/part/migrations/0015_auto_20190820_0251.py new file mode 100644 index 0000000000..b981358519 --- /dev/null +++ b/InvenTree/part/migrations/0015_auto_20190820_0251.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.4 on 2019-08-20 02:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0014_partparameter'), + ] + + operations = [ + migrations.CreateModel( + name='PartParameterTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Parameter Name', max_length=100)), + ('units', models.CharField(blank=True, help_text='Parameter Units', max_length=25)), + ], + ), + migrations.RemoveField( + model_name='partparameter', + name='name', + ), + migrations.AlterField( + model_name='partparameter', + name='data', + field=models.CharField(help_text='Parameter Value', max_length=500), + ), + migrations.AddField( + model_name='partparameter', + name='template', + field=models.ForeignKey(default=1, help_text='Parameter Template', on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='part.PartParameterTemplate'), + preserve_default=False, + ), + ] diff --git a/InvenTree/part/migrations/0016_auto_20190820_0257.py b/InvenTree/part/migrations/0016_auto_20190820_0257.py new file mode 100644 index 0000000000..0399129078 --- /dev/null +++ b/InvenTree/part/migrations/0016_auto_20190820_0257.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-08-20 02:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0015_auto_20190820_0251'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='partparameter', + unique_together={('part', 'template')}, + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 2e9e748e99..382eccdf07 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -971,6 +971,11 @@ class Part(models.Model): return sum([part.on_order() for part in self.supplier_parts.all().prefetch_related('purchase_order_line_items')]) + def get_parameters(self): + """ Return all parameters for this part, ordered by name """ + + return self.parameters.order_by('template__name') + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment @@ -1028,6 +1033,81 @@ class PartStar(models.Model): unique_together = ['part', 'user'] +class PartParameterTemplate(models.Model): + """ + A PartParameterTemplate provides a template for key:value pairs for extra + parameters fields/values to be added to a Part. + This allows users to arbitrarily assign data fields to a Part + beyond the built-in attributes. + + Attributes: + name: The name (key) of the Parameter [string] + units: The units of the Parameter [string] + """ + + def __str__(self): + s = str(self.name) + if self.units: + s += " ({units})".format(units=self.units) + return s + + def validate_unique(self, exclude=None): + """ Ensure that PartParameterTemplates cannot be created with the same name. + This test should be case-insensitive (which the unique caveat does not cover). + """ + + super().validate_unique(exclude) + + try: + others = PartParameterTemplate.objects.exclude(id=self.id).filter(name__iexact=self.name) + + if others.exists(): + msg = _("Parameter template name must be unique") + raise ValidationError({"name": msg}) + except PartParameterTemplate.DoesNotExist: + pass + + @property + def instance_count(self): + """ Return the number of instances of this Parameter Template """ + return self.instances.count() + + name = models.CharField(max_length=100, help_text='Parameter Name') + + units = models.CharField(max_length=25, help_text='Parameter Units', blank=True) + + +class PartParameter(models.Model): + """ + A PartParameter is a specific instance of a PartParameterTemplate. It assigns a particular parameter pair to a part. + + Attributes: + part: Reference to a single Part object + template: Reference to a single PartParameterTemplate object + data: The data (value) of the Parameter [string] + """ + + def __str__(self): + # String representation of a PartParameter (used in the admin interface) + return "{part} : {param} = {data}{units}".format( + part=str(self.part), + param=str(self.template.name), + data=str(self.data), + units=str(self.template.units) + ) + + class Meta: + # Prevent multiple instances of a parameter for a single part + unique_together = ('part', 'template') + + part = models.ForeignKey(Part, on_delete=models.CASCADE, + related_name='parameters', help_text='Parent Part') + + template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE, related_name='instances', help_text='Parameter Template') + + data = models.CharField(max_length=500, help_text='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 diff --git a/InvenTree/part/templates/part/param_delete.html b/InvenTree/part/templates/part/param_delete.html new file mode 100644 index 0000000000..efb8ca3c26 --- /dev/null +++ b/InvenTree/part/templates/part/param_delete.html @@ -0,0 +1,5 @@ +{% extends "modal_delete_form.html" %} + +{% block pre_form_content %} +Are you sure you want to remove this parameter? +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html new file mode 100644 index 0000000000..1ccb9d4da6 --- /dev/null +++ b/InvenTree/part/templates/part/params.html @@ -0,0 +1,80 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% block details %} + +{% include "part/tabs.html" with tab='params' %} + +

Part Parameters

+
+ +
+
+ +
+
+ + + + + + + + + + + {% for param in part.get_parameters %} + + + + + + {% endfor %} + +
NameValueUnits
{{ param.template.name }}{{ param.data }} + {{ param.template.units }} +
+ + +
+
+ + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + $('#param-table').bootstrapTable({ + search: true, + sortable: true, + }); + + $('#param-create').click(function() { + launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", { + reload: true, + secondary: [{ + field: 'template', + label: 'New Template', + title: 'Create New Parameter Template', + url: "{% url 'part-param-template-create' %}" + }], + }); + }); + + $('.param-edit').click(function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + reload: true, + }); + }); + + $('.param-delete').click(function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + reload: true, + }); + }); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index aa7a9fa885..86b7986b00 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -2,6 +2,9 @@ Details + + Parameters {{ part.parameters.count }} + {% if part.is_template %} Variants {{ part.variants.count }} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 3ec99e7255..d1fd1d59fc 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -18,6 +18,16 @@ part_attachment_urls = [ url(r'^(?P\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), ] +part_parameter_urls = [ + + url('^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), + + url('^new/', views.PartParameterCreate.as_view(), name='part-param-create'), + url('^(?P\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), + url('^(?P\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), + +] + part_detail_urls = [ url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), @@ -29,6 +39,7 @@ part_detail_urls = [ url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), + url(r'^params/', views.PartDetail.as_view(template_name='part/params.html'), name='part-params'), url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'), @@ -90,6 +101,9 @@ part_urls = [ # Part attachments url(r'^attachment/', include(part_attachment_urls)), + # Part parameters + url(r'^parameter/', include(part_parameter_urls)), + # Change category for multiple parts url(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 3b475cec7b..cc1300238e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -19,6 +19,7 @@ import tablib from fuzzywuzzy import fuzz from .models import PartCategory, Part, PartAttachment +from .models import PartParameterTemplate, PartParameter from .models import BomItem from .models import match_part_names @@ -1396,6 +1397,86 @@ class PartPricing(AjaxView): return self.renderJsonResponse(request, self.form_class(), data=data, context=self.get_pricing(quantity)) +class PartParameterTemplateCreate(AjaxCreateView): + """ View for creating a new PartParameterTemplate """ + + model = PartParameterTemplate + form_class = part_forms.EditPartParameterTemplateForm + ajax_form_title = 'Create Part Parameter Template' + + +class PartParameterCreate(AjaxCreateView): + """ View for creating a new PartParameter """ + + model = PartParameter + form_class = part_forms.EditPartParameterForm + ajax_form_title = 'Create Part Parameter' + + def get_initial(self): + + initials = {} + + part_id = self.request.GET.get('part', None) + + if part_id: + try: + initials['part'] = Part.objects.get(pk=part_id) + except (Part.DoesNotExist, ValueError): + pass + + return initials + + def get_form(self): + """ Return the form object. + + - Hide the 'Part' field (specified in URL) + - Limit the 'Template' options (to avoid duplicates) + """ + + form = super().get_form() + + part_id = self.request.GET.get('part', None) + + if part_id: + try: + part = Part.objects.get(pk=part_id) + + form.fields['part'].widget = HiddenInput() + + query = form.fields['template'].queryset + + query = query.exclude(id__in=[param.template.id for param in part.parameters.all()]) + + form.fields['template'].queryset = query + + except (Part.DoesNotExist, ValueError): + pass + + return form + + +class PartParameterEdit(AjaxUpdateView): + """ View for editing a PartParameter """ + + model = PartParameter + form_class = part_forms.EditPartParameterForm + ajax_form_title = 'Edit Part Parameter' + + def get_form(self): + + form = super().get_form() + + return form + + +class PartParameterDelete(AjaxDeleteView): + """ View for deleting a PartParameter """ + + model = PartParameter + ajax_template_name = 'part/param_delete.html' + ajax_form_title = 'Delete Part Parameter' + + class CategoryDetail(DetailView): """ Detail view for PartCategory """ model = PartCategory