mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 19:45:46 +00:00
Related parts are now created via the API
This commit is contained in:
@ -157,20 +157,6 @@ class BomMatchItemForm(MatchItemForm):
|
|||||||
return super().get_special_field(col_guess, row, file_manager)
|
return super().get_special_field(col_guess, row, file_manager)
|
||||||
|
|
||||||
|
|
||||||
class CreatePartRelatedForm(HelperForm):
|
|
||||||
""" Form for creating a PartRelated object """
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PartRelated
|
|
||||||
fields = [
|
|
||||||
'part_1',
|
|
||||||
'part_2',
|
|
||||||
]
|
|
||||||
labels = {
|
|
||||||
'part_2': _('Related Part'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SetPartCategoryForm(forms.Form):
|
class SetPartCategoryForm(forms.Form):
|
||||||
""" Form for setting the category of multiple Part objects """
|
""" Form for setting the category of multiple Part objects """
|
||||||
|
|
||||||
|
@ -759,11 +759,25 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
$("#add-related-part").click(function() {
|
$("#add-related-part").click(function() {
|
||||||
launchModalForm("{% url 'part-related-create' %}", {
|
|
||||||
data: {
|
constructForm('{% url "api-part-related-list" %}', {
|
||||||
part: {{ part.id }},
|
method: 'POST',
|
||||||
|
fields: {
|
||||||
|
part_1: {
|
||||||
|
hidden: true,
|
||||||
|
value: {{ part.pk }},
|
||||||
},
|
},
|
||||||
reload: true,
|
part_2: {
|
||||||
|
label: '{% trans "Related Part" %}',
|
||||||
|
filters: {
|
||||||
|
exclude_related: {{ part.pk }},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: '{% trans "Add Related Part" %}',
|
||||||
|
onSuccess: function() {
|
||||||
|
$('#related-parts-table').bootstrapTable('refresh');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -145,36 +145,6 @@ class PartDetailTest(PartViewTestCase):
|
|||||||
self.assertIn('streaming_content', dir(response))
|
self.assertIn('streaming_content', dir(response))
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedTests(PartViewTestCase):
|
|
||||||
|
|
||||||
def test_valid_create(self):
|
|
||||||
""" test creation of a related part """
|
|
||||||
|
|
||||||
# Test GET view
|
|
||||||
response = self.client.get(reverse('part-related-create'), {'part': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# Test POST view with valid form data
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 2},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": true', status_code=200)
|
|
||||||
|
|
||||||
# Try to create the same relationship with part_1 and part_2 pks reversed
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 2, 'part_2': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": false', status_code=200)
|
|
||||||
|
|
||||||
# Try to create part related to itself
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": false', status_code=200)
|
|
||||||
|
|
||||||
# Check final count
|
|
||||||
n = PartRelated.objects.all().count()
|
|
||||||
self.assertEqual(n, 1)
|
|
||||||
|
|
||||||
|
|
||||||
class PartQRTest(PartViewTestCase):
|
class PartQRTest(PartViewTestCase):
|
||||||
""" Tests for the Part QR Code AJAX view """
|
""" Tests for the Part QR Code AJAX view """
|
||||||
|
|
||||||
|
@ -12,10 +12,6 @@ from django.conf.urls import url, include
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
part_related_urls = [
|
|
||||||
url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'),
|
|
||||||
url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'),
|
|
||||||
]
|
|
||||||
|
|
||||||
sale_price_break_urls = [
|
sale_price_break_urls = [
|
||||||
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
||||||
@ -96,9 +92,6 @@ part_urls = [
|
|||||||
# Part category
|
# Part category
|
||||||
url(r'^category/', include(category_urls)),
|
url(r'^category/', include(category_urls)),
|
||||||
|
|
||||||
# Part related
|
|
||||||
url(r'^related-parts/', include(part_related_urls)),
|
|
||||||
|
|
||||||
# Part price breaks
|
# Part price breaks
|
||||||
url(r'^sale-price/', include(sale_price_break_urls)),
|
url(r'^sale-price/', include(sale_price_break_urls)),
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import io
|
|||||||
from rapidfuzz import fuzz
|
from rapidfuzz import fuzz
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
from .models import PartCategory, Part, PartRelated
|
from .models import PartCategory, Part
|
||||||
from .models import PartParameterTemplate
|
from .models import PartParameterTemplate
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
@ -85,75 +85,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedCreate(AjaxCreateView):
|
|
||||||
""" View for creating a new PartRelated object
|
|
||||||
|
|
||||||
- The view only makes sense if a Part object is passed to it
|
|
||||||
"""
|
|
||||||
model = PartRelated
|
|
||||||
form_class = part_forms.CreatePartRelatedForm
|
|
||||||
ajax_form_title = _("Add Related Part")
|
|
||||||
ajax_template_name = "modal_form.html"
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
""" Set parent part as part_1 field """
|
|
||||||
|
|
||||||
initials = {}
|
|
||||||
|
|
||||||
part_id = self.request.GET.get('part', None)
|
|
||||||
|
|
||||||
if part_id:
|
|
||||||
try:
|
|
||||||
initials['part_1'] = Part.objects.get(pk=part_id)
|
|
||||||
except (Part.DoesNotExist, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
""" Create a form to upload a new PartRelated
|
|
||||||
|
|
||||||
- Hide the 'part_1' field (parent part)
|
|
||||||
- Display parts which are not yet related
|
|
||||||
"""
|
|
||||||
|
|
||||||
form = super(AjaxCreateView, self).get_form()
|
|
||||||
|
|
||||||
form.fields['part_1'].widget = HiddenInput()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get parent part
|
|
||||||
parent_part = self.get_initial()['part_1']
|
|
||||||
# Get existing related parts
|
|
||||||
related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()]
|
|
||||||
|
|
||||||
# Build updated choice list excluding
|
|
||||||
# - parts already related to parent part
|
|
||||||
# - the parent part itself
|
|
||||||
updated_choices = []
|
|
||||||
for choice in form.fields["part_2"].choices:
|
|
||||||
if (choice[0] not in related_parts) and (choice[0] != parent_part.pk):
|
|
||||||
updated_choices.append(choice)
|
|
||||||
|
|
||||||
# Update choices for related part
|
|
||||||
form.fields['part_2'].choices = updated_choices
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedDelete(AjaxDeleteView):
|
|
||||||
""" View for deleting a PartRelated object """
|
|
||||||
|
|
||||||
model = PartRelated
|
|
||||||
ajax_form_title = _("Delete Related Part")
|
|
||||||
context_object_name = "related"
|
|
||||||
|
|
||||||
# Explicit role requirement
|
|
||||||
role_required = 'part.change'
|
|
||||||
|
|
||||||
|
|
||||||
class PartSetCategory(AjaxUpdateView):
|
class PartSetCategory(AjaxUpdateView):
|
||||||
""" View for settings the part category for multiple parts at once """
|
""" View for settings the part category for multiple parts at once """
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user