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:
+
+{% for match in matches %}
+-
+ {{ match.part.full_name }} - {{ match.part.description }} ({{ match.ratio }}%)
+
+{% endfor %}
+
+{% 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 %}
-
+
{{ form.pre_form_info }}
{% endif %}