Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-08-28 19:48:46 +10:00
commit 0dce5fab7b
11 changed files with 372 additions and 6 deletions

View File

@ -4,6 +4,7 @@ from import_export.admin import ImportExportModelAdmin
from .models import PartCategory, Part from .models import PartCategory, Part
from .models import PartAttachment, PartStar from .models import PartAttachment, PartStar
from .models import BomItem from .models import BomItem
from .models import PartParameterTemplate, PartParameter
class PartAdmin(ImportExportModelAdmin): class PartAdmin(ImportExportModelAdmin):
@ -30,17 +31,18 @@ class BomItemAdmin(ImportExportModelAdmin):
list_display = ('part', 'sub_part', 'quantity') list_display = ('part', 'sub_part', 'quantity')
""" class ParameterTemplateAdmin(ImportExportModelAdmin):
class ParameterTemplateAdmin(admin.ModelAdmin): list_display = ('name', 'units')
list_display = ('name', 'units', 'format')
class ParameterAdmin(admin.ModelAdmin): class ParameterAdmin(ImportExportModelAdmin):
list_display = ('part', 'template', 'value') list_display = ('part', 'template', 'data')
"""
admin.site.register(Part, PartAdmin) admin.site.register(Part, PartAdmin)
admin.site.register(PartCategory, PartCategoryAdmin) admin.site.register(PartCategory, PartCategoryAdmin)
admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin)
admin.site.register(PartStar, PartStarAdmin) admin.site.register(PartStar, PartStarAdmin)
admin.site.register(BomItem, BomItemAdmin) admin.site.register(BomItem, BomItemAdmin)
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
admin.site.register(PartParameter, ParameterAdmin)

View File

@ -11,6 +11,7 @@ from django import forms
from .models import Part, PartCategory, PartAttachment from .models import Part, PartCategory, PartAttachment
from .models import BomItem from .models import BomItem
from .models import PartParameterTemplate, PartParameter
class PartImageForm(HelperForm): 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): class EditCategoryForm(HelperForm):
""" Form for editing a PartCategory object """ """ Form for editing a PartCategory object """

View File

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

View File

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

View File

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

View File

@ -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')]) 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): def attach_file(instance, filename):
""" Function for storing a file for a PartAttachment """ Function for storing a file for a PartAttachment
@ -1028,6 +1033,81 @@ class PartStar(models.Model):
unique_together = ['part', 'user'] 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 <key:value> 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): class BomItem(models.Model):
""" A BomItem links a part to its component items. """ A BomItem links a part to its component items.
A part can have a BOM (bill of materials) which defines A part can have a BOM (bill of materials) which defines

View File

@ -0,0 +1,5 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to remove this parameter?
{% endblock %}

View File

@ -0,0 +1,80 @@
{% extends "part/part_base.html" %}
{% load static %}
{% block details %}
{% include "part/tabs.html" with tab='params' %}
<h4>Part Parameters</h4>
<hr>
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<button class='btn btn-success' id='param-create'>New Parameter</button>
</div>
</div>
<table id='param-table' class='table table-condensed table-striped' data-toolbar='#button-toolbar'>
<thead>
<tr>
<th data-field='name' data-serachable='true'>Name</th>
<th data-field='value' data-searchable='true'>Value</th>
<th data-field='units' data-searchable='true'>Units</th>
</tr>
</thead>
<tbody>
{% for param in part.get_parameters %}
<tr>
<td>{{ param.template.name }}</td>
<td>{{ param.data }}</td>
<td>
{{ param.template.units }}
<div class='btn-group' style='float: right;'>
<button title='Edit' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='glyphicon glyphicon-edit'/></button>
<button title='Delete' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='glyphicon glyphicon-trash'/></button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% 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 %}

View File

@ -2,6 +2,9 @@
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}> <li{% ifequal tab 'detail' %} class="active"{% endifequal %}>
<a href="{% url 'part-detail' part.id %}">Details</a> <a href="{% url 'part-detail' part.id %}">Details</a>
</li> </li>
<li{% ifequal tab 'params' %} class='active'{% endifequal %}>
<a href="{% url 'part-params' part.id %}">Parameters <span class='badge'>{{ part.parameters.count }}</span></a>
</li>
{% if part.is_template %} {% if part.is_template %}
<li{% ifequal tab 'variants' %} class='active'{% endifequal %}> <li{% ifequal tab 'variants' %} class='active'{% endifequal %}>
<a href="{% url 'part-variants' part.id %}">Variants <span class='badge'>{{ part.variants.count }}</span></span></a> <a href="{% url 'part-variants' part.id %}">Variants <span class='badge'>{{ part.variants.count }}</span></span></a>

View File

@ -18,6 +18,16 @@ part_attachment_urls = [
url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), url(r'^(?P<pk>\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<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
url('^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'),
]
part_detail_urls = [ part_detail_urls = [
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), 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'^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'^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'^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'), url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'),
@ -90,6 +101,9 @@ part_urls = [
# Part attachments # Part attachments
url(r'^attachment/', include(part_attachment_urls)), url(r'^attachment/', include(part_attachment_urls)),
# Part parameters
url(r'^parameter/', include(part_parameter_urls)),
# Change category for multiple parts # Change category for multiple parts
url(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'), url(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'),

View File

@ -19,6 +19,7 @@ import tablib
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from .models import PartCategory, Part, PartAttachment from .models import PartCategory, Part, PartAttachment
from .models import PartParameterTemplate, PartParameter
from .models import BomItem from .models import BomItem
from .models import match_part_names 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)) 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): class CategoryDetail(DetailView):
""" Detail view for PartCategory """ """ Detail view for PartCategory """
model = PartCategory model = PartCategory