From e8978643962b7b348564e6293ac3b6cf3052d2ad Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 23 Mar 2021 17:01:54 -0400 Subject: [PATCH 01/43] Added ManufacturerPart model, form and views --- InvenTree/company/forms.py | 23 ++++- InvenTree/company/models.py | 90 +++++++++++++------ InvenTree/company/views.py | 171 ++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 29 deletions(-) diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 67ac402ba7..b9e8b607cf 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -17,6 +17,7 @@ from djmoney.forms.fields import MoneyField import common.settings from .models import Company +from .models import ManufacturerPart from .models import SupplierPart from .models import SupplierPriceBreak @@ -84,12 +85,30 @@ class CompanyImageDownloadForm(HelperForm): ] +class EditManufacturerPartForm(HelperForm): + """ Form for editing a ManufacturerPart object """ + + field_prefix = { + 'link': 'fa-link', + 'MPN': 'fa-hashtag', + } + + class Meta: + model = ManufacturerPart + fields = [ + 'part', + 'description', + 'manufacturer', + 'MPN', + 'link', + ] + + class EditSupplierPartForm(HelperForm): """ Form for editing a SupplierPart object """ field_prefix = { 'link': 'fa-link', - 'MPN': 'fa-hashtag', 'SKU': 'fa-hashtag', 'note': 'fa-pencil-alt', } @@ -110,8 +129,6 @@ class EditSupplierPartForm(HelperForm): 'supplier', 'SKU', 'description', - 'manufacturer', - 'MPN', 'link', 'note', 'single_pricing', diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index e4386712c8..8b41e44553 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -278,19 +278,75 @@ class Contact(models.Model): on_delete=models.CASCADE) -class SupplierPart(models.Model): - """ Represents a unique part as provided by a Supplier - Each SupplierPart is identified by a MPN (Manufacturer Part Number) - Each SupplierPart is also linked to a Part object. - A Part may be available from multiple suppliers +class MaufacturerPart(models.Model): + """ Represents a unique part as provided by a Manufacturer + Each MaufacturerPart is identified by a MPN (Manufacturer Part Number) + Each MaufacturerPart is also linked to a Part object. + A Part may be available from multiple manufacturers Attributes: part: Link to the master Part + manufacturer: Company that manufactures the MaufacturerPart + MPN: Manufacture part number + link: Link to external website for this manufacturer part + description: Descriptive notes field + """ + + class Meta: + unique_together = ('part', 'manufacturer', 'MPN') + + part = models.ForeignKey('part.Part', on_delete=models.CASCADE, + related_name='manufacturer_parts', + verbose_name=_('Base Part'), + limit_choices_to={ + 'purchaseable': True, + }, + help_text=_('Select part'), + ) + + manufacturer = models.ForeignKey( + Company, + on_delete=models.SET_NULL, + related_name='manufactured_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, + verbose_name=_('MPN'), + help_text=_('Manufacturer Part Number') + ) + + link = InvenTreeURLField( + blank=True, null=True, + verbose_name=_('Link'), + help_text=_('URL for external manufacturer part link') + ) + + description = models.CharField( + max_length=250, blank=True, null=True, + verbose_name=_('Description'), + help_text=_('Manufacturer part description') + ) + + +class SupplierPart(models.Model): + """ Represents a unique part as provided by a Supplier + Each SupplierPart is identified by a SKU (Supplier Part Number) + Each SupplierPart is also linked to a Part or ManufacturerPart object. + A Part may be available from multiple suppliers + + Attributes: + part_type: Part or ManufacturerPart + part_id: Part or ManufacturerPart ID supplier: Company that supplies this SupplierPart object SKU: Stock keeping unit (supplier part number) - manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!) - MPN: Manufacture part number - link: Link to external website for this part + link: Link to external website for this supplier part description: Descriptive notes field note: Longer form note field base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee" @@ -330,24 +386,6 @@ class SupplierPart(models.Model): help_text=_('Supplier stock keeping unit') ) - manufacturer = models.ForeignKey( - Company, - on_delete=models.SET_NULL, - related_name='manufactured_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, - verbose_name=_('MPN'), - help_text=_('Manufacturer part number') - ) - link = InvenTreeURLField( blank=True, null=True, verbose_name=_('Link'), diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 42457d6101..0786eb0ee8 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -24,6 +24,7 @@ from InvenTree.helpers import str2bool from InvenTree.views import InvenTreeRoleMixin from .models import Company +from .models import ManufacturerPart from .models import SupplierPart from .models import SupplierPriceBreak @@ -31,6 +32,7 @@ from part.models import Part from .forms import EditCompanyForm from .forms import CompanyImageForm +from .forms import EditManufacturerPartForm from .forms import EditSupplierPartForm from .forms import EditPriceBreakForm from .forms import CompanyImageDownloadForm @@ -331,6 +333,175 @@ class CompanyDelete(AjaxDeleteView): } +class ManufacturerPartDetail(DetailView): + """ Detail view for ManufacturerPart """ + model = ManufacturerPart + template_name = 'company/manufacturer_part_detail.html' + context_object_name = 'part' + queryset = ManufacturerPart.objects.all() + permission_required = 'purchase_order.view' + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + return ctx + + +class ManufacturerPartEdit(AjaxUpdateView): + """ Update view for editing ManufacturerPart """ + + model = ManufacturerPart + context_object_name = 'part' + form_class = EditSupplierPartForm + ajax_template_name = 'modal_form.html' + ajax_form_title = _('Edit Manufacturer Part') + + +class ManufacturerPartCreate(AjaxCreateView): + """ Create view for making new ManufacturerPart """ + + model = ManufacturerPart + form_class = EditManufacturerPartForm + ajax_template_name = 'company/manufacturer_part_create.html' + ajax_form_title = _('Create New Manufacturer Part') + context_object_name = 'part' + + def get_context_data(self): + """ + Supply context data to the form + """ + + ctx = super().get_context_data() + + # Add 'part' object + form = self.get_form() + + part = form['part'].value() + + try: + part = Part.objects.get(pk=part) + except (ValueError, Part.DoesNotExist): + part = None + + ctx['part'] = part + + return ctx + + def get_form(self): + """ Create Form instance to create a new ManufacturerPart object. + Hide some fields if they are not appropriate in context + """ + form = super(AjaxCreateView, self).get_form() + + if form.initial.get('part', None): + # Hide the part field + form.fields['part'].widget = HiddenInput() + + return form + + def get_initial(self): + """ Provide initial data for new ManufacturerPart: + + - If 'manufacturer_id' provided, pre-fill manufacturer field + - If 'part_id' provided, pre-fill part field + """ + initials = super(SupplierPartCreate, self).get_initial().copy() + + manufacturer_id = self.get_param('manufacturer') + part_id = self.get_param('part') + + if manufacturer_id: + try: + initials['manufacturer'] = Company.objects.get(pk=manufacturer_id) + except (ValueError, Company.DoesNotExist): + pass + + if part_id: + try: + initials['part'] = Part.objects.get(pk=part_id) + except (ValueError, Part.DoesNotExist): + pass + + +class ManufacturerPartDelete(AjaxDeleteView): + """ Delete view for removing a ManufacturerPart. + + ManufacturerParts can be deleted using a variety of 'selectors'. + + - ?part= -> Delete a single ManufacturerPart object + - ?parts=[] -> Delete a list of ManufacturerPart objects + + """ + + success_url = '/manufacturer/' + ajax_template_name = 'company/partdelete.html' + ajax_form_title = _('Delete Manufacturer Part') + + role_required = 'purchase_order.delete' + + parts = [] + + def get_context_data(self): + ctx = {} + + ctx['parts'] = self.parts + + return ctx + + def get_parts(self): + """ Determine which ManufacturerPart object(s) the user wishes to delete. + """ + + self.parts = [] + + # User passes a single ManufacturerPart ID + if 'part' in self.request.GET: + try: + self.parts.append(ManufacturerPart.objects.get(pk=self.request.GET.get('part'))) + except (ValueError, SupplierPart.DoesNotExist): + pass + + elif 'parts[]' in self.request.GET: + + part_id_list = self.request.GET.getlist('parts[]') + + self.parts = ManufacturerPart.objects.filter(id__in=part_id_list) + + def get(self, request, *args, **kwargs): + self.request = request + self.get_parts() + + return self.renderJsonResponse(request, form=self.get_form()) + + def post(self, request, *args, **kwargs): + """ Handle the POST action for deleting ManufacturerPart object. + """ + + self.request = request + self.parts = [] + + for item in self.request.POST: + if item.startswith('manufacturer-part-'): + pk = item.replace('manufacturer-part-', '') + + try: + self.parts.append(ManufacturerPart.objects.get(pk=pk)) + except (ValueError, ManufacturerPart.DoesNotExist): + pass + + confirm = str2bool(self.request.POST.get('confirm_delete', False)) + + data = { + 'form_valid': confirm, + } + + if confirm: + for part in self.parts: + part.delete() + + return self.renderJsonResponse(self.request, data=data, form=self.get_form()) + + class SupplierPartDetail(DetailView): """ Detail view for SupplierPart """ model = SupplierPart From 8f610d826f0ef5432f99ab620569392eb8ef98cf Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 23 Mar 2021 17:16:29 -0400 Subject: [PATCH 02/43] Added URLs and templates --- .../company/manufacturer_part_base.html | 131 ++++++++++++++++++ .../company/manufacturer_part_create.html | 17 +++ .../company/manufacturer_part_detail.html | 38 +++++ InvenTree/company/urls.py | 12 ++ 4 files changed, 198 insertions(+) create mode 100644 InvenTree/company/templates/company/manufacturer_part_base.html create mode 100644 InvenTree/company/templates/company/manufacturer_part_create.html create mode 100644 InvenTree/company/templates/company/manufacturer_part_detail.html diff --git a/InvenTree/company/templates/company/manufacturer_part_base.html b/InvenTree/company/templates/company/manufacturer_part_base.html new file mode 100644 index 0000000000..7b0b579d1d --- /dev/null +++ b/InvenTree/company/templates/company/manufacturer_part_base.html @@ -0,0 +1,131 @@ +{% extends "two_column.html" %} +{% load static %} +{% load i18n %} + +{% block page_title %} +InvenTree | {% trans "Manufacturer Part" %} +{% endblock %} + +{% block thumbnail %} + +{% endblock %} + +{% block page_data %} +

{% trans "Manufacturer Part" %}

+
+

+ {{ part.part.full_name }} + {% if user.is_staff and perms.company.change_company %} + + + + {% endif %} +

+

{{ part.manufacturer.name }} - {{ part.MPN }}

+ +{% if roles.purchase_order.change %} +
+
+ {% if roles.purchase_order.add %} + + {% endif %} + + {% if roles.purchase_order.delete %} + + {% endif %} +
+
+{% endif %} + +{% endblock %} + +{% block page_details %} + +

{% trans "Manufacturer Part Details" %}

+ + + + + + + + {% if part.description %} + + + + + + {% endif %} + {% if part.link %} + + + + + + {% endif %} + + + + + + + + + +
{% trans "Internal Part" %} + {% if part.part %} + {{ part.part.full_name }} + {% endif %} +
{% trans "Description" %}{{ part.description }}
{% trans "External Link" %}{{ part.link }}
{% trans "Manufacturer" %}{{ part.manufacturer.name }}
{% trans "MPN" %}{{ part.MPN }}
+{% endblock %} + +{% block js_ready %} +{{ block.super }} + +enableNavbar({ + label: 'manufacturer-part', + toggleId: '#manufacturer-part-menu-toggle' +}) + +$('#order-part, #order-part2').click(function() { + launchModalForm( + "{% url 'order-parts' %}", + { + data: { + part: {{ part.part.id }}, + }, + reload: true, + }, + ); +}); + +$('#edit-part').click(function () { + launchModalForm( + "{% url 'manufacturer-part-edit' part.id %}", + { + reload: true + } + ); +}); + +$('#delete-part').click(function() { + launchModalForm( + "{% url 'manufacturer-part-delete' %}?part={{ part.id }}", + { + redirect: "{% url 'company-detail-parts' part.manufacturer.id %}" + } + ); +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/manufacturer_part_create.html b/InvenTree/company/templates/company/manufacturer_part_create.html new file mode 100644 index 0000000000..21c23f9075 --- /dev/null +++ b/InvenTree/company/templates/company/manufacturer_part_create.html @@ -0,0 +1,17 @@ +{% extends "modal_form.html" %} + +{% load i18n %} + +{% block pre_form_content %} +{{ block.super }} + +{% if part %} +
+ {% include "hover_image.html" with image=part.image %} + {{ part.full_name}} +
+ {{ part.description }} +
+{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/manufacturer_part_detail.html b/InvenTree/company/templates/company/manufacturer_part_detail.html new file mode 100644 index 0000000000..f2bcfea963 --- /dev/null +++ b/InvenTree/company/templates/company/manufacturer_part_detail.html @@ -0,0 +1,38 @@ +{% extends "company/manufacturer_part_base.html" %} +{% load static %} +{% load i18n %} + +{% block menubar %} +{% include "company/part_navbar.html" with tab='details' %} +{% endblock %} + +{% block heading %} +{% trans "Manufacturer Part Details" %} +{% endblock %} + + +{% block details %} + + + + + + + + +{% if part.link %} + +{% endif %} +
{% trans "Internal Part" %} + {% if part.part %} + {{ part.part.full_name }} + {% endif %} +
{% trans "Manufacturer" %}{{ part.manufacturer.name }}
{% trans "MPN" %}{{ part.MPN }}
{% trans "External Link" %}{{ part.link }}
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index b5ad06019b..682b74841d 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -52,6 +52,18 @@ price_break_urls = [ url(r'^(?P\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'), ] +manufacturer_part_detail_urls = [ + url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='supplier-part-edit'), +] + +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'^(?P\d+)/', include(manufacturer_part_detail_urls)), +] + supplier_part_detail_urls = [ url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'), From 08ffbee8edf2c738ade147daa4c55e50d179653f Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 23 Mar 2021 17:33:29 -0400 Subject: [PATCH 03/43] Fixed model name and added to part navbar --- InvenTree/company/models.py | 8 ++++---- InvenTree/part/templates/part/navbar.html | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 8b41e44553..202453b9ae 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -278,15 +278,15 @@ class Contact(models.Model): on_delete=models.CASCADE) -class MaufacturerPart(models.Model): +class ManufacturerPart(models.Model): """ Represents a unique part as provided by a Manufacturer - Each MaufacturerPart is identified by a MPN (Manufacturer Part Number) - Each MaufacturerPart is also linked to a Part object. + Each ManufacturerPart is identified by a MPN (Manufacturer Part Number) + Each ManufacturerPart is also linked to a Part object. A Part may be available from multiple manufacturers Attributes: part: Link to the master Part - manufacturer: Company that manufactures the MaufacturerPart + manufacturer: Company that manufactures the ManufacturerPart MPN: Manufacture part number link: Link to external website for this manufacturer part description: Descriptive notes field diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html index 17e69172de..e621af038a 100644 --- a/InvenTree/part/templates/part/navbar.html +++ b/InvenTree/part/templates/part/navbar.html @@ -69,9 +69,15 @@ {% endif %} {% if part.purchaseable and roles.purchase_order.view %} -
  • +
  • + {% trans "Manufacturers" %} + +
  • +
  • + + {% trans "Suppliers" %}
  • From e28dde7f7bc914b0bd29e39725603932fd78d76f Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 23 Mar 2021 17:45:03 -0400 Subject: [PATCH 04/43] Fixed navbar, added missing template and urls --- InvenTree/InvenTree/urls.py | 2 + InvenTree/company/models.py | 20 +++- .../part/templates/part/manufacturer.html | 91 +++++++++++++++++++ InvenTree/part/templates/part/navbar.html | 2 +- InvenTree/part/urls.py | 1 + 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 InvenTree/part/templates/part/manufacturer.html diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index a9f53a7014..e74faf3a87 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -11,6 +11,7 @@ from django.contrib import admin from django.contrib.auth import views as auth_views from company.urls import company_urls +from company.urls import manufacturer_part_urls from company.urls import supplier_part_urls from company.urls import price_break_urls @@ -114,6 +115,7 @@ dynamic_javascript_urls = [ urlpatterns = [ url(r'^part/', include(part_urls)), + url(r'^manufacturer-part/', include(manufacturer_part_urls)), url(r'^supplier-part/', include(supplier_part_urls)), url(r'^price-break/', include(price_break_urls)), diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 202453b9ae..340cb1f91f 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -307,7 +307,7 @@ class ManufacturerPart(models.Model): manufacturer = models.ForeignKey( Company, on_delete=models.SET_NULL, - related_name='manufactured_parts', + related_name='manufacturer_parts', limit_choices_to={ 'is_manufacturer': True }, @@ -386,6 +386,24 @@ class SupplierPart(models.Model): help_text=_('Supplier stock keeping unit') ) + manufacturer = models.ForeignKey( + Company, + on_delete=models.SET_NULL, + related_name='manufactured_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, + verbose_name=_('MPN'), + help_text=_('Manufacturer part number') + ) + link = InvenTreeURLField( blank=True, null=True, verbose_name=_('Link'), diff --git a/InvenTree/part/templates/part/manufacturer.html b/InvenTree/part/templates/part/manufacturer.html new file mode 100644 index 0000000000..1e023c6349 --- /dev/null +++ b/InvenTree/part/templates/part/manufacturer.html @@ -0,0 +1,91 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} + +{% block menubar %} +{% include 'part/navbar.html' with tab='manufacturers' %} +{% endblock %} + +{% block heading %} +{% trans "Part Manufacturers" %} +{% endblock %} + +{% block details %} +
    +
    + +
    + + +
    +
    +
    + + +
    + +{% endblock %} + +{% block js_load %} +{{ block.super }} +{% endblock %} +{% block js_ready %} + {{ block.super }} + + $('#manufacturer-create').click(function () { + launchModalForm( + "{% url 'manufacturer-part-create' %}", + { + reload: true, + data: { + part: {{ part.id }} + }, + secondary: [ + { + field: 'manufacturer', + label: '{% trans "New Manufacturer" %}', + title: '{% trans "Create new manufacturer" %}', + url: "{% url 'manufacturer-create' %}", + } + ] + }); + }); + + $("#manufacturer-part-delete").click(function() { + + var selections = $("#manufacturer-table").bootstrapTable("getSelections"); + + var parts = []; + + selections.forEach(function(item) { + parts.push(item.pk); + }); + + launchModalForm("{% url 'manufacturer-part-delete' %}", { + data: { + parts: parts, + }, + reload: true, + }); + }); + + loadSupplierPartTable( + "#supplier-table", + "{% url 'api-supplier-part-list' %}", + { + params: { + part: {{ part.id }}, + part_detail: true, + supplier_detail: true, + manufacturer_detail: true, + }, + } + ); + + linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']) + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html index e621af038a..1c94c0358f 100644 --- a/InvenTree/part/templates/part/navbar.html +++ b/InvenTree/part/templates/part/navbar.html @@ -70,7 +70,7 @@ {% endif %} {% if part.purchaseable and roles.purchase_order.view %}
  • - + {% trans "Manufacturers" %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index f275edede2..f1185fbe8c 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -60,6 +60,7 @@ part_detail_urls = [ url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), + url(r'^manufacturers/?', views.PartDetail.as_view(template_name='part/manufacturer.html'), name='part-manufacturers'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'), url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'), From afd2dacfc75b0e98cb03f3141d9f1ac969d80693 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 24 Mar 2021 11:44:51 -0400 Subject: [PATCH 05/43] 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 From 4abd8587ab1c5ef06f33cdc73e669a51e963aedc Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 24 Mar 2021 12:08:45 -0400 Subject: [PATCH 06/43] Updated company templates --- .../company/templates/company/detail.html | 4 +- .../templates/company/detail_part.html | 74 +++++++++++++++---- .../company/templates/company/navbar.html | 13 +++- InvenTree/company/views.py | 2 +- 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index b803fc5852..735f34f6b3 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -51,12 +51,12 @@ - + {% trans "Manufacturer" %} {% include "yesnolabel.html" with value=company.is_manufacturer %} - + {% trans "Supplier" %} {% include 'yesnolabel.html' with value=company.is_supplier %} diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index b12322353a..65362521cd 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -17,9 +17,16 @@
    {% if roles.purchase_order.add %} - + {% if company.is_manufacturer %} + + {% endif %} + {% if company.is_supplier %} + + {% endif %} {% endif %}
  • - {% if company.is_supplier or company.is_manufacturer %} + {% if company.is_manufacturer %} +
  • + + + {% trans "Parts" %} + +
  • + {% endif %} + + {% if company.is_supplier %}
  • {% trans "Parts" %}
  • + {% endif %} + {% if company.is_manufacturer or company.is_supplier %}
  • diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index cdaca6a9e3..015e5e248c 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -352,7 +352,7 @@ class ManufacturerPartEdit(AjaxUpdateView): model = ManufacturerPart context_object_name = 'part' - form_class = EditSupplierPartForm + form_class = EditManufacturerPartForm ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Manufacturer Part') From fd4f33d45b877ed46ea07d769672639acfb13919 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 24 Mar 2021 12:39:26 -0400 Subject: [PATCH 07/43] More templates updates --- .../company/manufacturer_part_base.html | 2 ++ .../company/manufacturer_part_delete.html | 31 +++++++++++++++++++ .../company/manufacturer_part_detail.html | 2 +- .../company/manufacturer_part_navbar.html | 27 ++++++++++++++++ ...tdelete.html => supplier_part_delete.html} | 7 +++-- .../company/supplier_part_detail.html | 2 +- ..._navbar.html => supplier_part_navbar.html} | 0 .../company/supplier_part_orders.html | 2 +- .../company/supplier_part_pricing.html | 2 +- .../company/supplier_part_stock.html | 2 +- InvenTree/company/views.py | 6 ++-- 11 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 InvenTree/company/templates/company/manufacturer_part_delete.html create mode 100644 InvenTree/company/templates/company/manufacturer_part_navbar.html rename InvenTree/company/templates/company/{partdelete.html => supplier_part_delete.html} (92%) rename InvenTree/company/templates/company/{part_navbar.html => supplier_part_navbar.html} (100%) diff --git a/InvenTree/company/templates/company/manufacturer_part_base.html b/InvenTree/company/templates/company/manufacturer_part_base.html index 7b0b579d1d..5c6e6c6b1d 100644 --- a/InvenTree/company/templates/company/manufacturer_part_base.html +++ b/InvenTree/company/templates/company/manufacturer_part_base.html @@ -31,11 +31,13 @@ src="{% static 'img/blank_image.png' %}" {% if roles.purchase_order.change %}
    + {% comment "for later" %} {% if roles.purchase_order.add %} {% endif %} + {% endcomment %} diff --git a/InvenTree/company/templates/company/manufacturer_part_delete.html b/InvenTree/company/templates/company/manufacturer_part_delete.html new file mode 100644 index 0000000000..30eab3b591 --- /dev/null +++ b/InvenTree/company/templates/company/manufacturer_part_delete.html @@ -0,0 +1,31 @@ +{% extends "modal_delete_form.html" %} +{% load i18n %} + +{% block pre_form_content %} +{% trans "Are you sure you want to delete the following Manufacturer Parts?" %} + +
    +{% endblock %} + +{% block form_data %} + +{% for part in parts %} + + + + + + + +{% endfor %} +
    + {% include "hover_image.html" with image=part.part.image %} + {{ part.part.full_name }} + + {% include "hover_image.html" with image=part.manufacturer.image %} + {{ part.manufacturer.name }} + + {{ part.MPN }} +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/manufacturer_part_detail.html b/InvenTree/company/templates/company/manufacturer_part_detail.html index f2bcfea963..f351f955dc 100644 --- a/InvenTree/company/templates/company/manufacturer_part_detail.html +++ b/InvenTree/company/templates/company/manufacturer_part_detail.html @@ -3,7 +3,7 @@ {% load i18n %} {% block menubar %} -{% include "company/part_navbar.html" with tab='details' %} +{% include "company/manufacturer_part_navbar.html" with tab='details' %} {% endblock %} {% block heading %} diff --git a/InvenTree/company/templates/company/manufacturer_part_navbar.html b/InvenTree/company/templates/company/manufacturer_part_navbar.html new file mode 100644 index 0000000000..31430e7e6e --- /dev/null +++ b/InvenTree/company/templates/company/manufacturer_part_navbar.html @@ -0,0 +1,27 @@ +{% load i18n %} + +
    \ No newline at end of file diff --git a/InvenTree/company/templates/company/partdelete.html b/InvenTree/company/templates/company/supplier_part_delete.html similarity index 92% rename from InvenTree/company/templates/company/partdelete.html rename to InvenTree/company/templates/company/supplier_part_delete.html index 8eed487697..b44e7c0610 100644 --- a/InvenTree/company/templates/company/partdelete.html +++ b/InvenTree/company/templates/company/supplier_part_delete.html @@ -13,13 +13,16 @@ + + {% include "hover_image.html" with image=part.part.image %} + {{ part.part.full_name }} + {% include "hover_image.html" with image=part.supplier.image %} {{ part.supplier.name }} - {% include "hover_image.html" with image=part.part.image %} - {{ part.part.full_name }} + {{ part.SKU }} {% endfor %} diff --git a/InvenTree/company/templates/company/supplier_part_detail.html b/InvenTree/company/templates/company/supplier_part_detail.html index d3b94f0ad5..7a8ac0dcd3 100644 --- a/InvenTree/company/templates/company/supplier_part_detail.html +++ b/InvenTree/company/templates/company/supplier_part_detail.html @@ -3,7 +3,7 @@ {% load i18n %} {% block menubar %} -{% include "company/part_navbar.html" with tab='details' %} +{% include "company/supplier_part_navbar.html" with tab='details' %} {% endblock %} {% block heading %} diff --git a/InvenTree/company/templates/company/part_navbar.html b/InvenTree/company/templates/company/supplier_part_navbar.html similarity index 100% rename from InvenTree/company/templates/company/part_navbar.html rename to InvenTree/company/templates/company/supplier_part_navbar.html diff --git a/InvenTree/company/templates/company/supplier_part_orders.html b/InvenTree/company/templates/company/supplier_part_orders.html index ae4b025b11..932f61f30f 100644 --- a/InvenTree/company/templates/company/supplier_part_orders.html +++ b/InvenTree/company/templates/company/supplier_part_orders.html @@ -3,7 +3,7 @@ {% load i18n %} {% block menubar %} -{% include "company/part_navbar.html" with tab='orders' %} +{% include "company/supplier_part_navbar.html" with tab='orders' %} {% endblock %} {% block heading %} diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index 2314b5cfcf..a674837650 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -4,7 +4,7 @@ {% load inventree_extras %} {% block menubar %} -{% include "company/part_navbar.html" with tab='pricing' %} +{% include "company/supplier_part_navbar.html" with tab='pricing' %} {% endblock %} {% block heading %} diff --git a/InvenTree/company/templates/company/supplier_part_stock.html b/InvenTree/company/templates/company/supplier_part_stock.html index 49a5a809c2..170b9f8e82 100644 --- a/InvenTree/company/templates/company/supplier_part_stock.html +++ b/InvenTree/company/templates/company/supplier_part_stock.html @@ -3,7 +3,7 @@ {% load i18n %} {% block menubar %} -{% include "company/part_navbar.html" with tab='stock' %} +{% include "company/supplier_part_navbar.html" with tab='stock' %} {% endblock %} {% block heading %} diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 015e5e248c..dbcc5948da 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -436,7 +436,7 @@ class ManufacturerPartDelete(AjaxDeleteView): """ success_url = '/manufacturer/' - ajax_template_name = 'company/partdelete.html' + ajax_template_name = 'company/manufacturer_part_delete.html' ajax_form_title = _('Delete Manufacturer Part') role_required = 'purchase_order.delete' @@ -460,7 +460,7 @@ class ManufacturerPartDelete(AjaxDeleteView): if 'part' in self.request.GET: try: self.parts.append(ManufacturerPart.objects.get(pk=self.request.GET.get('part'))) - except (ValueError, SupplierPart.DoesNotExist): + except (ValueError, ManufacturerPart.DoesNotExist): pass elif 'parts[]' in self.request.GET: @@ -666,7 +666,7 @@ class SupplierPartDelete(AjaxDeleteView): """ success_url = '/supplier/' - ajax_template_name = 'company/partdelete.html' + ajax_template_name = 'company/supplier_part_delete.html' ajax_form_title = _('Delete Supplier Part') role_required = 'purchase_order.delete' From c85d7374469dc67ec77d9f9f2990d6be9bbedc4a Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 25 Mar 2021 15:04:48 -0400 Subject: [PATCH 08/43] Fix CI --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 5c95abfe46..3d0f2e9e7e 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -69,6 +69,7 @@ class RuleSet(models.Model): 'part_partrelated', 'part_partstar', 'company_supplierpart', + 'company_manufacturerpart', ], 'stock_location': [ 'stock_stocklocation', From e0a0bfdadb7731697e815c4462371b8cb748dd92 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 25 Mar 2021 15:39:16 -0400 Subject: [PATCH 09/43] Fix style --- InvenTree/company/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index b1fc81f0b0..7b02f5ec4a 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -7,7 +7,6 @@ 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 From e6dfb7da524073753506632e6b0b1ee14c21068b Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 29 Mar 2021 13:22:15 -0400 Subject: [PATCH 10/43] Added global setting to enable manufacturer parts Created SourceItem model Updated templates --- InvenTree/common/models.py | 7 ++++ InvenTree/company/api.py | 2 +- .../migrations/0033_auto_20210329_1700.py | 33 +++++++++++++++++++ InvenTree/company/models.py | 33 +++++++++++++++++-- .../templates/company/detail_part.html | 15 +++++---- .../company/templates/company/navbar.html | 8 ++--- InvenTree/part/templates/part/navbar.html | 3 ++ .../templates/InvenTree/settings/part.html | 2 ++ 8 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 InvenTree/company/migrations/0033_auto_20210329_1700.py diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index df8e3b2d37..a785e944bc 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -202,6 +202,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'PART_ENABLE_MANUFACTURER_PARTS': { + 'name': _('Enable Manufacturer Parts'), + 'description': _('Enable the use of manufacturer parts for purchasing'), + 'default': False, + 'validator': bool, + }, + 'REPORT_DEBUG_MODE': { 'name': _('Debug Mode'), 'description': _('Generate reports in debug mode (HTML output)'), diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index b0962afbc4..e0ac1b804a 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -130,7 +130,7 @@ class ManufacturerPartList(generics.ListCreateAPIView): params = self.request.query_params # Filter by manufacturer - manufacturer = params.get('manufacturer', None) + manufacturer = params.get('company', None) if manufacturer is not None: queryset = queryset.filter(manufacturer=manufacturer) diff --git a/InvenTree/company/migrations/0033_auto_20210329_1700.py b/InvenTree/company/migrations/0033_auto_20210329_1700.py new file mode 100644 index 0000000000..341ac95999 --- /dev/null +++ b/InvenTree/company/migrations/0033_auto_20210329_1700.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.7 on 2021-03-29 17:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('company', '0032_manufacturerpart'), + ] + + operations = [ + migrations.CreateModel( + name='SourceItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('part_id', models.PositiveIntegerField()), + ('part_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + migrations.AddField( + model_name='manufacturerpart', + name='source_item', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='manufacturer_parts', to='company.SourceItem', verbose_name='Sourcing Item'), + ), + migrations.AddField( + model_name='supplierpart', + name='source_item', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='company.SourceItem', verbose_name='Sourcing Item'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 05421c54bc..bd628c64d4 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -13,6 +13,8 @@ from django.utils.translation import gettext_lazy as _ from django.core.validators import MinValueValidator from django.db import models from django.db.models import Sum, Q, UniqueConstraint +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.apps import apps from django.urls import reverse @@ -278,6 +280,20 @@ class Contact(models.Model): on_delete=models.CASCADE) +class SourceItem(models.Model): + """ This model allows flexibility for sourcing of InvenTree parts. + Each SourceItem instance represents a single ManufacturerPart or + SupplierPart instance. + SourceItem can be linked to either Part or ManufacturerPart instances. + """ + + part_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + + part_id = models.PositiveIntegerField() + + part = GenericForeignKey('part_type', 'part_id') + + class ManufacturerPart(models.Model): """ Represents a unique part as provided by a Manufacturer Each ManufacturerPart is identified by a MPN (Manufacturer Part Number) @@ -286,6 +302,7 @@ class ManufacturerPart(models.Model): Attributes: part: Link to the master Part + source_item: The sourcing item linked to this ManufacturerPart instance manufacturer: Company that manufactures the ManufacturerPart MPN: Manufacture part number link: Link to external website for this manufacturer part @@ -303,6 +320,12 @@ class ManufacturerPart(models.Model): }, help_text=_('Select part'), ) + + source_item = models.ForeignKey(SourceItem, on_delete=models.CASCADE, + blank=True, null=True, + related_name='manufacturer_parts', + verbose_name=_('Sourcing Item'), + ) manufacturer = models.ForeignKey( Company, @@ -341,8 +364,8 @@ class SupplierPart(models.Model): A Part may be available from multiple suppliers Attributes: - part_type: Part or ManufacturerPart - part_id: Part or ManufacturerPart ID + part: Link to the master Part + source_item: The sourcing item linked to this SupplierPart instance supplier: Company that supplies this SupplierPart object SKU: Stock keeping unit (supplier part number) link: Link to external website for this supplier part @@ -372,6 +395,12 @@ class SupplierPart(models.Model): help_text=_('Select part'), ) + source_item = models.ForeignKey(SourceItem, on_delete=models.CASCADE, + blank=True, null=True, + related_name='supplier_parts', + verbose_name=_('Sourcing Item'), + ) + supplier = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='supplied_parts', limit_choices_to={'is_supplier': True}, diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index 65362521cd..9829213103 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -1,6 +1,7 @@ {% extends "company/company_base.html" %} {% load static %} {% load i18n %} +{% load inventree_extras %} {% block menubar %} {% include 'company/navbar.html' with tab='parts' %} @@ -12,17 +13,18 @@ {% block details %} +{% setting_object 'PART_ENABLE_MANUFACTURER_PARTS' as manufacturer_parts %} + {% if roles.purchase_order.change %}
    {% if roles.purchase_order.add %} - {% if company.is_manufacturer %} + {% if manufacturer_parts.value == "True" and company.is_manufacturer %} - {% endif %} - {% if company.is_supplier %} + {% else %} @@ -57,6 +59,7 @@ {% endblock %} {% block js_ready %} {{ block.super }} +{% setting_object 'PART_ENABLE_MANUFACTURER_PARTS' as manufacturer_parts %} $("#manufacturer-part-create").click(function () { launchModalForm( @@ -108,7 +111,7 @@ }); }); - {% if company.is_manufacturer %} + {% if manufacturer_parts.value == "True" and company.is_manufacturer %} loadManufacturerPartTable( "#part-table", "{% url 'api-manufacturer-part-list' %}", @@ -120,9 +123,7 @@ }, } ); - {% endif %} - - {% if company.is_supplier %} + {% else %} loadSupplierPartTable( "#part-table", "{% url 'api-supplier-part-list' %}", diff --git a/InvenTree/company/templates/company/navbar.html b/InvenTree/company/templates/company/navbar.html index 0527eb925e..ed6ff5799b 100644 --- a/InvenTree/company/templates/company/navbar.html +++ b/InvenTree/company/templates/company/navbar.html @@ -2,6 +2,8 @@ {% load static %} {% load inventree_extras %} +{% setting_object 'PART_ENABLE_MANUFACTURER_PARTS' as manufacturer_parts %} +