diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index b837b3da59..ed07318e28 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -116,7 +116,7 @@ class AjaxMixin(object): """ return {} - def renderJsonResponse(self, request, form=None, data={}, context={}): + def renderJsonResponse(self, request, form=None, data={}, context=None): """ Render a JSON response based on specific class context. Args: @@ -129,6 +129,12 @@ class AjaxMixin(object): JSON response object """ + if context is None: + try: + context = self.get_context_data() + except AttributeError: + context = {} + if form: context['form'] = form diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 2256f28210..580ed737a4 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -75,11 +75,20 @@ class EditPartAttachmentForm(HelperForm): class EditPartForm(HelperForm): """ Form for editing a Part object """ - confirm_creation = forms.BooleanField(required=False, initial=False, help_text='Confirm part creation', widget=forms.HiddenInput()) + deep_copy = forms.BooleanField(required=False, + initial=True, + help_text="Perform 'deep copy' which will duplicate all BOM data for this part", + widget=forms.HiddenInput()) + + confirm_creation = forms.BooleanField(required=False, + initial=False, + help_text='Confirm part creation', + widget=forms.HiddenInput()) class Meta: model = Part fields = [ + 'deep_copy', 'confirm_creation', 'category', 'name', diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d119c3094e..c057c7930e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -533,6 +533,19 @@ class Part(models.Model): """ Return the number of supplier parts available for this part """ return self.supplier_parts.count() + def copyBomFrom(self, other): + """ Duplicates the BOM from another part. + + This should only be called during part creation, + and it does not delete any existing BOM items for *this* part. + """ + + for item in other.bom_items.all(): + # Point the item to THIS part + item.part = self + item.pk = None + item.save() + def export_bom(self, **kwargs): data = tablib.Dataset(headers=[ diff --git a/InvenTree/part/templates/part/copy_part.html b/InvenTree/part/templates/part/copy_part.html new file mode 100644 index 0000000000..3abf0c5d54 --- /dev/null +++ b/InvenTree/part/templates/part/copy_part.html @@ -0,0 +1,24 @@ +{% extends "modal_form.html" %} + +{% block pre_form_content %} + +{{ block.super }} + +
+ Duplicate Part
+ Make a copy of part '{{ part.full_name }}'. +
+ +{% if matches %} +Possible Matching Parts +

The new part may be a duplicate of these existing parts:

+ +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index ddb0523a83..04e60c63cd 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -131,12 +131,9 @@ $("#duplicate-part").click(function() { launchModalForm( - "{% url 'part-create' %}", + "{% url 'part-duplicate' part.id %}", { follow: true, - data: { - copy: {{ part.id }}, - }, } ); }); diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index fb49af8a9b..0671240e73 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -36,6 +36,7 @@ part_detail_urls = [ url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'), + url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 135e3da144..3f5a16ef39 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -124,13 +124,124 @@ class PartAttachmentDelete(AjaxDeleteView): } +class PartDuplicate(AjaxCreateView): + """ View for duplicating an existing Part object. + + - Part is provided in the URL '/part//copy/' + - Option for 'deep-copy' which will duplicate all BOM items (default = True) + """ + + model = Part + form_class = part_forms.EditPartForm + + ajax_form_title = "Duplicate Part" + ajax_template_name = "part/copy_part.html" + + def get_data(self): + return { + 'success': 'Copied part' + } + + def get_part_to_copy(self): + try: + return Part.objects.get(id=self.kwargs['pk']) + except Part.DoesNotExist: + return None + + def get_context_data(self): + return { + 'part': self.get_part_to_copy() + } + + def get_form(self): + form = super(AjaxCreateView, self).get_form() + + # Force display of the 'deep_copy' widget + form.fields['deep_copy'].widget = CheckboxInput() + + return form + + def post(self, request, *args, **kwargs): + """ Capture the POST request for part duplication + + - If the deep_copy object is set, copy all the BOM items too! + """ + + form = self.get_form() + + context = self.get_context_data() + + valid = form.is_valid() + + name = request.POST.get('name', None) + + if name: + matches = match_part_names(name) + + if len(matches) > 0: + context['matches'] = matches + + # Enforce display of the checkbox + form.fields['confirm_creation'].widget = CheckboxInput() + + # Check if the user has checked the 'confirm_creation' input + confirmed = str2bool(request.POST.get('confirm_creation', False)) + + if not confirmed: + form.errors['confirm_creation'] = ['Possible matches exist - confirm creation of new part'] + + form.pre_form_warning = 'Possible matches exist - confirm creation of new part' + valid = False + + data = { + 'form_valid': valid + } + + if valid: + # Create the new Part + part = form.save() + + data['pk'] = part.pk + + deep_copy = str2bool(request.POST.get('deep_copy', False)) + + original = self.get_part_to_copy() + + if original: + if deep_copy: + part.copyBomFrom(original) + + try: + data['url'] = part.get_absolute_url() + except AttributeError: + pass + + if valid: + pass + + return self.renderJsonResponse(request, form, data, context=context) + + def get_initial(self): + """ Get initial data based on the Part to be copied from. + """ + + part = self.get_part_to_copy() + + if part: + initials = model_to_dict(part) + else: + initials = super(AjaxCreateView, self).get_initials() + + return initials + + + class PartCreate(AjaxCreateView): """ View for creating a new Part object. Options for providing initial conditions: - Provide a category object as initial data - - Copy an existing Part """ model = Part form_class = part_forms.EditPartForm @@ -224,22 +335,9 @@ class PartCreate(AjaxCreateView): """ Get initial data for the new Part object: - If a category is provided, pre-fill the Category field - - If 'copy' parameter is provided, copy from referenced Part """ - # Is the client attempting to copy an existing part? - part_to_copy = self.request.GET.get('copy', None) - - if part_to_copy: - try: - original = Part.objects.get(pk=part_to_copy) - initials = model_to_dict(original) - self.ajax_form_title = "Copy Part '{p}'".format(p=original.name) - except Part.DoesNotExist: - initials = super(PartCreate, self).get_initial() - - else: - initials = super(PartCreate, self).get_initial() + initials = super(PartCreate, self).get_initial() if self.get_category_id(): try: diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 7761addc55..e096a94994 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -162,6 +162,10 @@ z-index: 99999999; } +.js-modal-form .checkbox { + margin-left: 0px; +} + .modal-dialog { width: 45%; } diff --git a/InvenTree/templates/modal_form.html b/InvenTree/templates/modal_form.html index e25d2587da..1a99fc09ff 100644 --- a/InvenTree/templates/modal_form.html +++ b/InvenTree/templates/modal_form.html @@ -1,6 +1,6 @@
{% if form.pre_form_info %} -