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