From afd2dacfc75b0e98cb03f3141d9f1ac969d80693 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 24 Mar 2021 11:44:51 -0400 Subject: [PATCH] Can now create, view list of parts and view detail page --- InvenTree/company/api.py | 113 +++++++++++++++++- .../migrations/0032_manufacturerpart.py | 30 +++++ InvenTree/company/models.py | 5 +- InvenTree/company/serializers.py | 44 +++++++ InvenTree/company/urls.py | 4 +- InvenTree/company/views.py | 4 +- .../part/templates/part/manufacturer.html | 11 +- InvenTree/templates/js/company.js | 97 +++++++++++++++ 8 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 InvenTree/company/migrations/0032_manufacturerpart.py diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index ebf2924d16..b0962afbc4 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -15,9 +15,11 @@ from django.db.models import Q from InvenTree.helpers import str2bool from .models import Company +from .models import ManufacturerPart from .models import SupplierPart, SupplierPriceBreak from .serializers import CompanySerializer +from .serializers import ManufacturerPartSerializer from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer @@ -80,8 +82,107 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView): queryset = CompanySerializer.annotate_queryset(queryset) return queryset + + +class ManufacturerPartList(generics.ListCreateAPIView): + """ API endpoint for list view of ManufacturerPart object + + - GET: Return list of ManufacturerPart objects + - POST: Create a new ManufacturerPart object + """ + + queryset = ManufacturerPart.objects.all().prefetch_related( + 'part', + 'manufacturer', + ) + + serializer_class = ManufacturerPartSerializer + + def get_serializer(self, *args, **kwargs): + + # Do we wish to include extra detail? + try: + kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None)) + except AttributeError: + pass + + try: + kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None)) + except AttributeError: + pass + + try: + kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', 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=manufacturer) + + # Filter by parent part? + part = params.get('part', None) + + if part is not None: + queryset = queryset.filter(part=part) + + # Filter by 'active' status of the part? + active = params.get('active', None) + + if active is not None: + active = str2bool(active) + queryset = queryset.filter(part__active=active) + + return queryset + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + + filter_fields = [ + ] + + search_fields = [ + 'manufacturer__name', + 'description', + 'MPN', + 'part__name', + 'part__description', + ] +class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView): + """ API endpoint for detail view of ManufacturerPart object + + - GET: Retrieve detail view + - PATCH: Update object + - DELETE: Delete object + """ + + queryset = ManufacturerPart.objects.all() + serializer_class = ManufacturerPartSerializer + + read_only_fields = [ + ] + + class SupplierPartList(generics.ListCreateAPIView): """ API endpoint for list view of SupplierPart object @@ -226,6 +327,15 @@ class SupplierPriceBreakList(generics.ListCreateAPIView): ] +manufacturer_part_api_urls = [ + + url(r'^(?P\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'), + + # Catch anything else + url(r'^.*$', ManufacturerPartList.as_view(), name='api-manufacturer-part-list'), +] + + supplier_part_api_urls = [ url(r'^(?P\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), @@ -236,7 +346,8 @@ supplier_part_api_urls = [ company_api_urls = [ - + url(r'^part/manufacturer/', include(manufacturer_part_api_urls)), + url(r'^part/', include(supplier_part_api_urls)), url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'), diff --git a/InvenTree/company/migrations/0032_manufacturerpart.py b/InvenTree/company/migrations/0032_manufacturerpart.py new file mode 100644 index 0000000000..5547aec36d --- /dev/null +++ b/InvenTree/company/migrations/0032_manufacturerpart.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.7 on 2021-03-24 14:18 + +import InvenTree.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0063_bomitem_inherited'), + ('company', '0031_auto_20210103_2215'), + ] + + operations = [ + migrations.CreateModel( + name='ManufacturerPart', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('MPN', models.CharField(help_text='Manufacturer Part Number', max_length=100, verbose_name='MPN')), + ('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='URL for external manufacturer part link', null=True, verbose_name='Link')), + ('description', models.CharField(blank=True, help_text='Manufacturer part description', max_length=250, null=True, verbose_name='Description')), + ('manufacturer', models.ForeignKey(help_text='Select manufacturer', limit_choices_to={'is_manufacturer': True}, on_delete=django.db.models.deletion.CASCADE, related_name='manufacturer_parts', to='company.Company', verbose_name='Manufacturer')), + ('part', models.ForeignKey(help_text='Select part', limit_choices_to={'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='manufacturer_parts', to='part.Part', verbose_name='Base Part')), + ], + options={ + 'unique_together': {('part', 'manufacturer', 'MPN')}, + }, + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 340cb1f91f..05421c54bc 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -306,18 +306,17 @@ class ManufacturerPart(models.Model): manufacturer = models.ForeignKey( Company, - on_delete=models.SET_NULL, + on_delete=models.CASCADE, related_name='manufacturer_parts', limit_choices_to={ 'is_manufacturer': True }, verbose_name=_('Manufacturer'), help_text=_('Select manufacturer'), - null=True, blank=True ) MPN = models.CharField( - max_length=100, blank=True, null=True, + max_length=100, verbose_name=_('MPN'), help_text=_('Manufacturer Part Number') ) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 4951bd3ad0..b1fc81f0b0 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -7,6 +7,7 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount from .models import Company +from .models import ManufacturerPart from .models import SupplierPart, SupplierPriceBreak from InvenTree.serializers import InvenTreeModelSerializer @@ -80,6 +81,49 @@ class CompanySerializer(InvenTreeModelSerializer): ] +class ManufacturerPartSerializer(InvenTreeModelSerializer): + """ Serializer for SupplierPart object """ + + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + + manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True) + + pretty_name = serializers.CharField(read_only=True) + + def __init__(self, *args, **kwargs): + + part_detail = kwargs.pop('part_detail', False) + manufacturer_detail = kwargs.pop('manufacturer_detail', False) + prettify = kwargs.pop('pretty', False) + + super(ManufacturerPartSerializer, self).__init__(*args, **kwargs) + + if part_detail is not True: + self.fields.pop('part_detail') + + if manufacturer_detail is not True: + self.fields.pop('manufacturer_detail') + + if prettify is not True: + self.fields.pop('pretty_name') + + manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True)) + + class Meta: + model = SupplierPart + fields = [ + 'pk', + 'part', + 'part_detail', + 'pretty_name', + 'manufacturer', + 'manufacturer_detail', + 'description', + 'MPN', + 'link', + ] + + class SupplierPartSerializer(InvenTreeModelSerializer): """ Serializer for SupplierPart object """ diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 682b74841d..bb0fd1606a 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -53,7 +53,9 @@ price_break_urls = [ ] manufacturer_part_detail_urls = [ - url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='supplier-part-edit'), + url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'), + + url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_detail.html'), name='manufacturer-part-detail'), ] manufacturer_part_urls = [ diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 0786eb0ee8..cdaca6a9e3 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -405,7 +405,7 @@ class ManufacturerPartCreate(AjaxCreateView): - If 'manufacturer_id' provided, pre-fill manufacturer field - If 'part_id' provided, pre-fill part field """ - initials = super(SupplierPartCreate, self).get_initial().copy() + initials = super(ManufacturerPartCreate, self).get_initial().copy() manufacturer_id = self.get_param('manufacturer') part_id = self.get_param('part') @@ -422,6 +422,8 @@ class ManufacturerPartCreate(AjaxCreateView): except (ValueError, Part.DoesNotExist): pass + return initials + class ManufacturerPartDelete(AjaxDeleteView): """ Delete view for removing a ManufacturerPart. diff --git a/InvenTree/part/templates/part/manufacturer.html b/InvenTree/part/templates/part/manufacturer.html index 1e023c6349..09793ffc7f 100644 --- a/InvenTree/part/templates/part/manufacturer.html +++ b/InvenTree/part/templates/part/manufacturer.html @@ -14,7 +14,7 @@
@@ -73,19 +73,18 @@ }); }); - loadSupplierPartTable( - "#supplier-table", - "{% url 'api-supplier-part-list' %}", + loadManufacturerPartTable( + "#manufacturer-table", + "{% url 'api-manufacturer-part-list' %}", { params: { part: {{ part.id }}, part_detail: true, - supplier_detail: true, manufacturer_detail: true, }, } ); - linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']) + linkButtonsToSelection($("#manufacturer-table"), ['#manufacturer-part-options']) {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index 601d4a5370..1dda96e27f 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -101,6 +101,103 @@ function loadCompanyTable(table, url, options={}) { } +function loadManufacturerPartTable(table, url, options) { + /* + * Load manufacturer part table + * + */ + + // Query parameters + var params = options.params || {}; + + // Load filters + var filters = loadTableFilters("manufacturer-part"); + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("manufacturer-part", $(table)); + + $(table).inventreeTable({ + url: url, + method: 'get', + original: params, + queryParams: filters, + name: 'manufacturerparts', + groupBy: false, + formatNoMatches: function() { return "{% trans "No manufacturer parts found" %}"; }, + columns: [ + { + checkbox: true, + switchable: false, + }, + { + sortable: true, + field: 'part_detail.full_name', + title: '{% trans "Part" %}', + switchable: false, + formatter: function(value, row, index, field) { + + var url = `/part/${row.part}/`; + + var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url); + + if (row.part_detail.is_template) { + html += ``; + } + + if (row.part_detail.assembly) { + html += ``; + } + + if (!row.part_detail.active) { + html += `{% trans "Inactive" %}`; + } + + return html; + } + }, + { + sortable: true, + field: 'manufacturer', + title: '{% trans "Manufacturer" %}', + formatter: function(value, row, index, field) { + if (value && row.manufacturer_detail) { + var name = row.manufacturer_detail.name; + var url = `/company/${value}/`; + var html = imageHoverIcon(row.manufacturer_detail.image) + renderLink(name, url); + + return html; + } else { + return "-"; + } + } + }, + { + sortable: true, + field: 'MPN', + title: '{% trans "MPN" %}', + formatter: function(value, row, index, field) { + return renderLink(value, `/manufacturer-part/${row.pk}/`); + } + }, + { + field: 'link', + title: '{% trans "Link" %}', + formatter: function(value, row, index, field) { + if (value) { + return renderLink(value, value); + } else { + return ''; + } + } + }, + ], + }); +} + + function loadSupplierPartTable(table, url, options) { /* * Load supplier part table