mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #318 from SchrodingersGat/deep-part-copy
Deep part copy
This commit is contained in:
commit
4860b88927
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -16,6 +16,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models, transaction
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
@ -533,6 +534,33 @@ class Part(models.Model):
|
||||
""" Return the number of supplier parts available for this part """
|
||||
return self.supplier_parts.count()
|
||||
|
||||
def deepCopy(self, other, **kwargs):
|
||||
""" Duplicates non-field data from another part.
|
||||
Does not alter the normal fields of this part,
|
||||
but can be used to copy other data linked by ForeignKey refernce.
|
||||
|
||||
Keyword Args:
|
||||
image: If True, copies Part image (default = True)
|
||||
bom: If True, copies BOM data (default = False)
|
||||
"""
|
||||
|
||||
# Copy the part image
|
||||
if kwargs.get('image', True):
|
||||
image_file = ContentFile(other.image.read())
|
||||
image_file.name = rename_part_image(self, 'test.png')
|
||||
|
||||
self.image = image_file
|
||||
|
||||
# Copy the BOM data
|
||||
if kwargs.get('bom', False):
|
||||
for item in other.bom_items.all():
|
||||
# Point the item to THIS part
|
||||
item.part = self
|
||||
item.pk = None
|
||||
item.save()
|
||||
|
||||
self.save()
|
||||
|
||||
def export_bom(self, **kwargs):
|
||||
|
||||
data = tablib.Dataset(headers=[
|
||||
|
24
InvenTree/part/templates/part/copy_part.html
Normal file
24
InvenTree/part/templates/part/copy_part.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "modal_form.html" %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
<div class='alert alert-info alert-block'>
|
||||
<b>Duplicate Part</b><br>
|
||||
Make a copy of part '{{ part.full_name }}'.
|
||||
</div>
|
||||
|
||||
{% if matches %}
|
||||
<b>Possible Matching Parts</b>
|
||||
<p>The new part may be a duplicate of these existing parts:</p>
|
||||
<ul class='list-group'>
|
||||
{% for match in matches %}
|
||||
<li class='list-group-item list-group-item-condensed'>
|
||||
{{ match.part.full_name }} - <i>{{ match.part.description }}</i> ({{ match.ratio }}%)
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -14,8 +14,8 @@
|
||||
<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if part.active %}
|
||||
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
|
||||
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
|
||||
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
|
||||
<hr>
|
||||
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
|
||||
{% else %}
|
||||
@ -131,12 +131,9 @@
|
||||
|
||||
$("#duplicate-part").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'part-create' %}",
|
||||
"{% url 'part-duplicate' part.id %}",
|
||||
{
|
||||
follow: true,
|
||||
data: {
|
||||
copy: {{ part.id }},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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'),
|
||||
|
@ -124,13 +124,122 @@ class PartAttachmentDelete(AjaxDeleteView):
|
||||
}
|
||||
|
||||
|
||||
class PartDuplicate(AjaxCreateView):
|
||||
""" View for duplicating an existing Part object.
|
||||
|
||||
- Part <pk> is provided in the URL '/part/<pk>/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:
|
||||
part.deepCopy(original, bom=deep_copy)
|
||||
|
||||
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_initial()
|
||||
|
||||
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 +333,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:
|
||||
|
@ -162,6 +162,10 @@
|
||||
z-index: 99999999;
|
||||
}
|
||||
|
||||
.js-modal-form .checkbox {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 45%;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div>
|
||||
{% if form.pre_form_info %}
|
||||
<div class='alert alert-info' role='alert' style='display: block;'>
|
||||
<div class='alert alert-info alert-block' role='alert'>
|
||||
{{ form.pre_form_info }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
Loading…
Reference in New Issue
Block a user