2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-18 02:36:31 +00:00

Merge pull request from SchrodingersGat/supplier-part-parameters

Adds "parameters" for manufacturer parts
This commit is contained in:
Oliver
2021-06-21 16:39:15 +10:00
committed by GitHub
19 changed files with 588 additions and 24 deletions

@@ -20,9 +20,12 @@ v4 -> 2021-06-01
- BOM items can now accept "variant stock" to be assigned against them
- Many slight API tweaks were needed to get this to work properly!
v5 -> 2021-06-21
- Adds API interface for manufacturer part parameters
"""
INVENTREE_API_VERSION = 4
INVENTREE_API_VERSION = 5
def inventreeInstanceName():

@@ -11,6 +11,7 @@ import import_export.widgets as widgets
from .models import Company
from .models import SupplierPart
from .models import SupplierPriceBreak
from .models import ManufacturerPart, ManufacturerPartParameter
from part.models import Part
@@ -71,6 +72,92 @@ class SupplierPartAdmin(ImportExportModelAdmin):
]
class ManufacturerPartResource(ModelResource):
"""
Class for managing ManufacturerPart data import/export
"""
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
part_name = Field(attribute='part__full_name', readonly=True)
manufacturer = Field(attribute='manufacturer', widget=widgets.ForeignKeyWidget(Company))
manufacturer_name = Field(attribute='manufacturer__name', readonly=True)
class Meta:
model = ManufacturerPart
skip_unchanged = True
report_skipped = True
clean_model_instances = True
class ManufacturerPartParameterInline(admin.TabularInline):
"""
Inline for editing ManufacturerPartParameter objects,
directly from the ManufacturerPart admin view.
"""
model = ManufacturerPartParameter
class SupplierPartInline(admin.TabularInline):
"""
Inline for the SupplierPart model
"""
model = SupplierPart
class ManufacturerPartAdmin(ImportExportModelAdmin):
"""
Admin class for ManufacturerPart model
"""
resource_class = ManufacturerPartResource
list_display = ('part', 'manufacturer', 'MPN')
search_fields = [
'manufacturer__name',
'part__name',
'MPN',
]
inlines = [
SupplierPartInline,
ManufacturerPartParameterInline,
]
class ManufacturerPartParameterResource(ModelResource):
"""
Class for managing ManufacturerPartParameter data import/export
"""
class Meta:
model = ManufacturerPartParameter
skip_unchanged = True
report_skipped = True
clean_model_instance = True
class ManufacturerPartParameterAdmin(ImportExportModelAdmin):
"""
Admin class for ManufacturerPartParameter model
"""
resource_class = ManufacturerPartParameterResource
list_display = ('manufacturer_part', 'name', 'value')
search_fields = [
'manufacturer_part__manufacturer__name',
'name',
'value'
]
class SupplierPriceBreakResource(ModelResource):
""" Class for managing SupplierPriceBreak data import/export """
@@ -103,3 +190,6 @@ class SupplierPriceBreakAdmin(ImportExportModelAdmin):
admin.site.register(Company, CompanyAdmin)
admin.site.register(SupplierPart, SupplierPartAdmin)
admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin)
admin.site.register(ManufacturerPart, ManufacturerPartAdmin)
admin.site.register(ManufacturerPartParameter, ManufacturerPartParameterAdmin)

@@ -15,11 +15,11 @@ from django.db.models import Q
from InvenTree.helpers import str2bool
from .models import Company
from .models import ManufacturerPart
from .models import ManufacturerPart, ManufacturerPartParameter
from .models import SupplierPart, SupplierPriceBreak
from .serializers import CompanySerializer
from .serializers import ManufacturerPartSerializer
from .serializers import ManufacturerPartSerializer, ManufacturerPartParameterSerializer
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
@@ -175,6 +175,86 @@ class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ManufacturerPartSerializer
class ManufacturerPartParameterList(generics.ListCreateAPIView):
"""
API endpoint for list view of ManufacturerPartParamater model.
"""
queryset = ManufacturerPartParameter.objects.all()
serializer_class = ManufacturerPartParameterSerializer
def get_serializer(self, *args, **kwargs):
# Do we wish to include any extra detail?
try:
params = self.request.query_params
optional_fields = [
'manufacturer_part_detail',
]
for key in optional_fields:
kwargs[key] = str2bool(params.get(key, None))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def filter_queryset(self, queryset):
"""
Custom filtering for the queryset
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by manufacturer?
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer)
# Filter by part?
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(manufacturer_part__part=part)
return queryset
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
'name',
'value',
'units',
'manufacturer_part',
]
search_fields = [
'name',
'value',
'units',
]
class ManufacturerPartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of ManufacturerPartParameter model
"""
queryset = ManufacturerPartParameter.objects.all()
serializer_class = ManufacturerPartParameterSerializer
class SupplierPartList(generics.ListCreateAPIView):
""" API endpoint for list view of SupplierPart object
@@ -249,7 +329,7 @@ class SupplierPartList(generics.ListCreateAPIView):
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', None))
kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', None))
kwargs['manufacturer_detail'] = str2bool(self.params.get('manufacturer_detail', None))
kwargs['manufacturer_detail'] = str2bool(params.get('manufacturer_detail', None))
kwargs['pretty'] = str2bool(params.get('pretty', None))
except AttributeError:
pass
@@ -316,6 +396,13 @@ class SupplierPriceBreakList(generics.ListCreateAPIView):
manufacturer_part_api_urls = [
url(r'^parameter/', include([
url(r'^(?P<pk>\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'),
# Catch anything else
url(r'^.*$', ManufacturerPartParameterList.as_view(), name='api-manufacturer-part-parameter-list'),
])),
url(r'^(?P<pk>\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'),
# Catch anything else

@@ -16,7 +16,7 @@ from djmoney.forms.fields import MoneyField
from common.settings import currency_code_default
from .models import Company
from .models import Company, ManufacturerPartParameter
from .models import ManufacturerPart
from .models import SupplierPart
from .models import SupplierPriceBreak
@@ -105,6 +105,21 @@ class EditManufacturerPartForm(HelperForm):
]
class EditManufacturerPartParameterForm(HelperForm):
"""
Form for creating / editing a ManufacturerPartParameter object
"""
class Meta:
model = ManufacturerPartParameter
fields = [
'manufacturer_part',
'name',
'value',
'units',
]
class EditSupplierPartForm(HelperForm):
""" Form for editing a SupplierPart object """

@@ -0,0 +1,27 @@
# Generated by Django 3.2.4 on 2021-06-20 07:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0037_supplierpart_update_3'),
]
operations = [
migrations.CreateModel(
name='ManufacturerPartParameter',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Parameter name', max_length=500, verbose_name='Name')),
('value', models.CharField(help_text='Parameter value', max_length=500, verbose_name='Value')),
('units', models.CharField(blank=True, help_text='Parameter units', max_length=64, null=True, verbose_name='Units')),
('manufacturer_part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameters', to='company.manufacturerpart', verbose_name='Manufacturer Part')),
],
options={
'unique_together': {('manufacturer_part', 'name')},
},
),
]

@@ -371,6 +371,47 @@ class ManufacturerPart(models.Model):
return s
class ManufacturerPartParameter(models.Model):
"""
A ManufacturerPartParameter represents a key:value parameter for a MnaufacturerPart.
This is used to represent parmeters / properties for a particular manufacturer part.
Each parameter is a simple string (text) value.
"""
class Meta:
unique_together = ('manufacturer_part', 'name')
manufacturer_part = models.ForeignKey(
ManufacturerPart,
on_delete=models.CASCADE,
related_name='parameters',
verbose_name=_('Manufacturer Part'),
)
name = models.CharField(
max_length=500,
blank=False,
verbose_name=_('Name'),
help_text=_('Parameter name')
)
value = models.CharField(
max_length=500,
blank=False,
verbose_name=_('Value'),
help_text=_('Parameter value')
)
units = models.CharField(
max_length=64,
blank=True, null=True,
verbose_name=_('Units'),
help_text=_('Parameter units')
)
class SupplierPart(models.Model):
""" Represents a unique part as provided by a Supplier
Each SupplierPart is identified by a SKU (Supplier Part Number)

@@ -7,7 +7,7 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount
from .models import Company
from .models import ManufacturerPart
from .models import ManufacturerPart, ManufacturerPartParameter
from .models import SupplierPart, SupplierPriceBreak
from InvenTree.serializers import InvenTreeModelSerializer
@@ -124,6 +124,35 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer):
]
class ManufacturerPartParameterSerializer(InvenTreeModelSerializer):
"""
Serializer for the ManufacturerPartParameter model
"""
manufacturer_part_detail = ManufacturerPartSerializer(source='manufacturer_part', many=False, read_only=True)
def __init__(self, *args, **kwargs):
man_detail = kwargs.pop('manufacturer_part_detail', False)
super(ManufacturerPartParameterSerializer, self).__init__(*args, **kwargs)
if not man_detail:
self.fields.pop('manufacturer_part_detail')
class Meta:
model = ManufacturerPartParameter
fields = [
'pk',
'manufacturer_part',
'manufacturer_part_detail',
'name',
'value',
'units',
]
class SupplierPartSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPart object """

@@ -7,7 +7,7 @@
{% endblock %}
{% block heading %}
{% trans "Supplier Parts" %}
{% trans "Suppliers" %}
{% endblock %}
{% block details %}
@@ -30,9 +30,44 @@
{% endblock %}
{% block post_content_panels %}
<div class='panel panel-default panel-inventree'>
<div class='panel-heading'>
<h4>{% trans "Parameters" %}</h4>
</div>
<div class='panel-content'>
<div id='parameter-toolbar'>
<div class='btn-group'>
<button class='btn btn-success' id='parameter-create'>
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
</button>
<div id='param-dropdown' class='btn-group'>
<!-- TODO -->
</div>
</div>
</div>
<table class='table table-striped table-condensed' id='parameter-table' data-toolbar='#parameter-toolbar'></table>
</div>
</div>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('#parameter-create').click(function() {
launchModalForm(
"{% url 'manufacturer-part-parameter-create' %}",
{
data: {
manufacturer_part: {{ part.id }},
}
}
);
});
$('#supplier-create').click(function () {
launchModalForm(
"{% url 'supplier-part-create' %}",
@@ -84,6 +119,16 @@ loadSupplierPartTable(
}
);
loadManufacturerPartParameterTable(
"#parameter-table",
"{% url 'api-manufacturer-part-parameter-list' %}",
{
params: {
manufacturer_part: {{ part.id }},
}
}
);
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
{% endblock %}

@@ -53,20 +53,25 @@ price_break_urls = [
url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
]
manufacturer_part_detail_urls = [
url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'),
url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'),
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),
]
manufacturer_part_urls = [
url(r'^new/?', views.ManufacturerPartCreate.as_view(), name='manufacturer-part-create'),
url(r'delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'),
url(r'^delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'),
url(r'^(?P<pk>\d+)/', include(manufacturer_part_detail_urls)),
# URLs for ManufacturerPartParameter views (create / edit / delete)
url(r'^parameter/', include([
url(r'^new/', views.ManufacturerPartParameterCreate.as_view(), name='manufacturer-part-parameter-create'),
url(r'^(?P<pk>\d)/', include([
url(r'^edit/', views.ManufacturerPartParameterEdit.as_view(), name='manufacturer-part-parameter-edit'),
url(r'^delete/', views.ManufacturerPartParameterDelete.as_view(), name='manufacturer-part-parameter-delete'),
])),
])),
url(r'^(?P<pk>\d+)/', include([
url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'),
url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'),
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),
])),
]
supplier_part_detail_urls = [

@@ -23,14 +23,14 @@ from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.helpers import str2bool
from InvenTree.views import InvenTreeRoleMixin
from .models import Company
from .models import Company, ManufacturerPartParameter
from .models import ManufacturerPart
from .models import SupplierPart
from .models import SupplierPriceBreak
from part.models import Part
from .forms import EditCompanyForm
from .forms import EditCompanyForm, EditManufacturerPartParameterForm
from .forms import CompanyImageForm
from .forms import EditManufacturerPartForm
from .forms import EditSupplierPartForm
@@ -504,6 +504,66 @@ class ManufacturerPartDelete(AjaxDeleteView):
return self.renderJsonResponse(self.request, data=data, form=self.get_form())
class ManufacturerPartParameterCreate(AjaxCreateView):
"""
View for creating a new ManufacturerPartParameter object
"""
model = ManufacturerPartParameter
form_class = EditManufacturerPartParameterForm
ajax_form_title = _('Add Manufacturer Part Parameter')
def get_form(self):
form = super().get_form()
# Hide the manufacturer_part field if specified
if form.initial.get('manufacturer_part', None):
form.fields['manufacturer_part'].widget = HiddenInput()
return form
def get_initial(self):
initials = super().get_initial().copy()
manufacturer_part = self.get_param('manufacturer_part')
if manufacturer_part:
try:
initials['manufacturer_part'] = ManufacturerPartParameter.objects.get(pk=manufacturer_part)
except (ValueError, ManufacturerPartParameter.DoesNotExist):
pass
return initials
class ManufacturerPartParameterEdit(AjaxUpdateView):
"""
View for editing a ManufacturerPartParameter object
"""
model = ManufacturerPartParameter
form_class = EditManufacturerPartParameterForm
ajax_form_title = _('Edit Manufacturer Part Parameter')
def get_form(self):
form = super().get_form()
form.fields['manufacturer_part'].widget = HiddenInput()
return form
class ManufacturerPartParameterDelete(AjaxDeleteView):
"""
View for deleting a ManufacturerPartParameter object
"""
model = ManufacturerPartParameter
class SupplierPartDetail(DetailView):
""" Detail view for SupplierPart """
model = SupplierPart

@@ -111,6 +111,13 @@ class PartCategoryResource(ModelResource):
PartCategory.objects.rebuild()
class PartCategoryInline(admin.TabularInline):
"""
Inline for PartCategory model
"""
model = PartCategory
class PartCategoryAdmin(ImportExportModelAdmin):
resource_class = PartCategoryResource
@@ -119,6 +126,10 @@ class PartCategoryAdmin(ImportExportModelAdmin):
search_fields = ('name', 'description')
inlines = [
PartCategoryInline,
]
class PartRelatedAdmin(admin.ModelAdmin):
''' Class to manage PartRelated objects '''

@@ -380,7 +380,6 @@ class Part(MPTTModel):
previous.image.delete(save=False)
self.clean()
self.validate_unique()
super().save(*args, **kwargs)
@@ -672,6 +671,8 @@ class Part(MPTTModel):
super().clean()
self.validate_unique()
if self.trackable:
for part in self.get_used_in().all():

@@ -5,6 +5,7 @@ over and above the built-in Django tags.
"""
import os
import sys
from django.utils.translation import ugettext_lazy as _
from django.conf import settings as djangosettings
@@ -114,6 +115,14 @@ def inventree_title(*args, **kwargs):
return version.inventreeInstanceTitle()
@register.simple_tag()
def python_version(*args, **kwargs):
"""
Return the current python version
"""
return sys.version.split(' ')[0]
@register.simple_tag()
def inventree_version(*args, **kwargs):
""" Return InvenTree version string """

@@ -44,6 +44,13 @@ class LocationResource(ModelResource):
StockLocation.objects.rebuild()
class LocationInline(admin.TabularInline):
"""
Inline for sub-locations
"""
model = StockLocation
class LocationAdmin(ImportExportModelAdmin):
resource_class = LocationResource
@@ -52,6 +59,10 @@ class LocationAdmin(ImportExportModelAdmin):
search_fields = ('name', 'description')
inlines = [
LocationInline,
]
class StockItemResource(ModelResource):
""" Class for managing StockItem data import/export """

@@ -34,6 +34,11 @@
<td>{% trans "API Version" %}</td>
<td>{% inventree_api_version %}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Python Version" %}</td>
<td>{% python_version %}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Django Version" %}</td>

@@ -126,7 +126,7 @@ function loadManufacturerPartTable(table, url, options) {
queryParams: filters,
name: 'manufacturerparts',
groupBy: false,
formatNoMatches: function() { return "{% trans "No manufacturer parts found" %}"; },
formatNoMatches: function() { return '{% trans "No manufacturer parts found" %}'; },
columns: [
{
checkbox: true,
@@ -199,6 +199,107 @@ function loadManufacturerPartTable(table, url, options) {
}
function loadManufacturerPartParameterTable(table, url, options) {
/*
* Load table of ManufacturerPartParameter objects
*/
var params = options.params || {};
// Load filters
var filters = loadTableFilters("manufacturer-part-parameters");
// Overwrite explicit parameters
for (var key in params) {
filters[key] = params[key];
}
// setupFilterList("manufacturer-part-parameters", $(table));
$(table).inventreeTable({
url: url,
method: 'get',
original: params,
queryParams: filters,
name: 'manufacturerpartparameters',
groupBy: false,
formatNoMatches: function() { return '{% trans "No parameters found" %}'; },
columns: [
{
checkbox: true,
switchable: false,
visible: false,
},
{
field: 'name',
title: '{% trans "Name" %}',
switchable: false,
sortable: true,
},
{
field: 'value',
title: '{% trans "Value" %}',
switchable: false,
sortable: true,
},
{
field: 'units',
title: '{% trans "Units" %}',
switchable: true,
sortable: true,
},
{
field: 'actions',
title: '',
switchable: false,
sortable: false,
formatter: function(value, row) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-parameter-edit', pk, '{% trans "Edit parameter" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-parameter-delete', pk, '{% trans "Delete parameter" %}');
html += `</div>`;
return html;
}
}
],
onPostBody: function() {
// Setup callback functions
$(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/manufacturer-part/parameter/${pk}/edit/`,
{
success: function() {
$(table).bootstrapTable('refresh');
}
}
);
});
$(table).find('.button-parameter-delete').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/manufacturer-part/parameter/${pk}/delete/`,
{
success: function() {
$(table).bootstrapTable('refresh');
}
}
);
});
}
});
}
function loadSupplierPartTable(table, url, options) {
/*
* Load supplier part table
@@ -224,7 +325,7 @@ function loadSupplierPartTable(table, url, options) {
queryParams: filters,
name: 'supplierparts',
groupBy: false,
formatNoMatches: function() { return "{% trans "No supplier parts found" %}"; },
formatNoMatches: function() { return '{% trans "No supplier parts found" %}'; },
columns: [
{
checkbox: true,
@@ -260,7 +361,7 @@ function loadSupplierPartTable(table, url, options) {
{
sortable: true,
field: 'supplier',
title: "{% trans "Supplier" %}",
title: '{% trans "Supplier" %}',
formatter: function(value, row, index, field) {
if (value) {
var name = row.supplier_detail.name;
@@ -276,7 +377,7 @@ function loadSupplierPartTable(table, url, options) {
{
sortable: true,
field: 'SKU',
title: "{% trans "Supplier Part" %}",
title: '{% trans "Supplier Part" %}',
formatter: function(value, row, index, field) {
return renderLink(value, `/supplier-part/${row.pk}/`);
}

@@ -43,6 +43,9 @@
</div>
{% endblock %}
{% block pre_content_panels %}
{% endblock %}
{% block content_panels %}
<div class='panel panel-default panel-inventree'>
<div class='panel-heading'>
@@ -63,6 +66,9 @@
</div>
{% endblock %}
{% block post_content_panels %}
{% endblock %}
{% endblock %}
{% block js_ready %}

@@ -85,6 +85,7 @@ class RuleSet(models.Model):
'part_partstar',
'company_supplierpart',
'company_manufacturerpart',
'company_manufacturerpartparameter',
],
'stock_location': [
'stock_stocklocation',
@@ -116,6 +117,8 @@ class RuleSet(models.Model):
'order_purchaseorderattachment',
'order_purchaseorderlineitem',
'company_supplierpart',
'company_manufacturerpart',
'company_manufacturerpartparameter',
],
'sales_order': [
'company_company',

@@ -365,6 +365,21 @@ def import_records(c, filename='data.json'):
print("Data import completed")
@task
def delete_data(c, force=False):
"""
Delete all database records!
Warning: This will REALLY delete all records in the database!!
"""
if force:
manage(c, 'flush --noinput')
else:
manage(c, 'flush')
@task(post=[rebuild])
def import_fixtures(c):
"""