mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 21:25:42 +00:00 
			
		
		
		
	Merge pull request #1904 from SchrodingersGat/part-forms
Refactor Part creation and editing forms
This commit is contained in:
		| @@ -85,8 +85,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, instance=None, data=empty, **kwargs): | ||||
|  | ||||
|         # self.instance = instance | ||||
|         """ | ||||
|         Custom __init__ routine to ensure that *default* values (as specified in the ORM) | ||||
|         are used by the DRF serializers, *if* the values are not provided by the user. | ||||
|         """ | ||||
|  | ||||
|         # If instance is None, we are creating a new instance | ||||
|         if instance is None and data is not empty: | ||||
| @@ -193,7 +195,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): | ||||
|         try: | ||||
|             instance.full_clean() | ||||
|         except (ValidationError, DjangoValidationError) as exc: | ||||
|             raise ValidationError(detail=serializers.as_serializer_error(exc)) | ||||
|  | ||||
|             data = exc.message_dict | ||||
|  | ||||
|             # Change '__all__' key (django style) to 'non_field_errors' (DRF style) | ||||
|             if '__all__' in data: | ||||
|                 data['non_field_errors'] = data['__all__'] | ||||
|                 del data['__all__'] | ||||
|  | ||||
|             raise ValidationError(data) | ||||
|  | ||||
|         return data | ||||
|  | ||||
|   | ||||
| @@ -637,7 +637,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): | ||||
|         'PART_PURCHASEABLE': { | ||||
|             'name': _('Purchaseable'), | ||||
|             'description': _('Parts are purchaseable by default'), | ||||
|             'default': False, | ||||
|             'default': True, | ||||
|             'validator': bool, | ||||
|         }, | ||||
|  | ||||
| @@ -662,6 +662,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): | ||||
|             'validator': bool, | ||||
|         }, | ||||
|  | ||||
|         # TODO: Remove this setting in future, new API forms make this not useful | ||||
|         'PART_SHOW_QUANTITY_IN_FORMS': { | ||||
|             'name': _('Show Quantity in Forms'), | ||||
|             'description': _('Display available part quantity in some forms'), | ||||
|   | ||||
| @@ -23,6 +23,7 @@ from djmoney.money import Money | ||||
| from djmoney.contrib.exchange.models import convert_money | ||||
| from djmoney.contrib.exchange.exceptions import MissingRate | ||||
|  | ||||
| from decimal import Decimal | ||||
|  | ||||
| from .models import Part, PartCategory, BomItem | ||||
| from .models import PartParameter, PartParameterTemplate | ||||
| @@ -30,6 +31,7 @@ from .models import PartAttachment, PartTestTemplate | ||||
| from .models import PartSellPriceBreak, PartInternalPriceBreak | ||||
| from .models import PartCategoryParameterTemplate | ||||
|  | ||||
| from stock.models import StockItem | ||||
| from common.models import InvenTreeSetting | ||||
| from build.models import Build | ||||
|  | ||||
| @@ -628,16 +630,75 @@ class PartList(generics.ListCreateAPIView): | ||||
|         else: | ||||
|             return Response(data) | ||||
|  | ||||
|     def perform_create(self, serializer): | ||||
|     def create(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         We wish to save the user who created this part! | ||||
|  | ||||
|         Note: Implementation copied from DRF class CreateModelMixin | ||||
|         """ | ||||
|  | ||||
|         serializer = self.get_serializer(data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|  | ||||
|         part = serializer.save() | ||||
|         part.creation_user = self.request.user | ||||
|         part.save() | ||||
|  | ||||
|         # Optionally copy templates from category or parent category | ||||
|         copy_templates = { | ||||
|             'main': str2bool(request.data.get('copy_category_templates', False)), | ||||
|             'parent': str2bool(request.data.get('copy_parent_templates', False)) | ||||
|         } | ||||
|  | ||||
|         part.save(**{'add_category_templates': copy_templates}) | ||||
|  | ||||
|         # Optionally copy data from another part (e.g. when duplicating) | ||||
|         copy_from = request.data.get('copy_from', None) | ||||
|  | ||||
|         if copy_from is not None: | ||||
|  | ||||
|             try: | ||||
|                 original = Part.objects.get(pk=copy_from) | ||||
|  | ||||
|                 copy_bom = str2bool(request.data.get('copy_bom', False)) | ||||
|                 copy_parameters = str2bool(request.data.get('copy_parameters', False)) | ||||
|                 copy_image = str2bool(request.data.get('copy_image', True)) | ||||
|  | ||||
|                 # Copy image? | ||||
|                 if copy_image: | ||||
|                     part.image = original.image | ||||
|                     part.save() | ||||
|  | ||||
|                 # Copy BOM? | ||||
|                 if copy_bom: | ||||
|                     part.copy_bom_from(original) | ||||
|  | ||||
|                 # Copy parameter data? | ||||
|                 if copy_parameters: | ||||
|                     part.copy_parameters_from(original) | ||||
|  | ||||
|             except (ValueError, Part.DoesNotExist): | ||||
|                 pass | ||||
|  | ||||
|         # Optionally create initial stock item | ||||
|         try: | ||||
|             initial_stock = Decimal(request.data.get('initial_stock', 0)) | ||||
|  | ||||
|             if initial_stock > 0 and part.default_location is not None: | ||||
|  | ||||
|                 stock_item = StockItem( | ||||
|                     part=part, | ||||
|                     quantity=initial_stock, | ||||
|                     location=part.default_location, | ||||
|                 ) | ||||
|  | ||||
|                 stock_item.save(user=request.user) | ||||
|  | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         headers = self.get_success_headers(serializer.data) | ||||
|  | ||||
|         return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) | ||||
|  | ||||
|     def get_queryset(self, *args, **kwargs): | ||||
|  | ||||
|   | ||||
| @@ -177,82 +177,6 @@ class SetPartCategoryForm(forms.Form): | ||||
|     part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category')) | ||||
|  | ||||
|  | ||||
| class EditPartForm(HelperForm): | ||||
|     """ | ||||
|     Form for editing a Part object. | ||||
|     """ | ||||
|  | ||||
|     field_prefix = { | ||||
|         'keywords': 'fa-key', | ||||
|         'link': 'fa-link', | ||||
|         'IPN': 'fa-hashtag', | ||||
|         'default_expiry': 'fa-stopwatch', | ||||
|     } | ||||
|  | ||||
|     bom_copy = forms.BooleanField(required=False, | ||||
|                                   initial=True, | ||||
|                                   help_text=_("Duplicate all BOM data for this part"), | ||||
|                                   label=_('Copy BOM'), | ||||
|                                   widget=forms.HiddenInput()) | ||||
|  | ||||
|     parameters_copy = forms.BooleanField(required=False, | ||||
|                                          initial=True, | ||||
|                                          help_text=_("Duplicate all parameter data for this part"), | ||||
|                                          label=_('Copy Parameters'), | ||||
|                                          widget=forms.HiddenInput()) | ||||
|  | ||||
|     confirm_creation = forms.BooleanField(required=False, | ||||
|                                           initial=False, | ||||
|                                           help_text=_('Confirm part creation'), | ||||
|                                           widget=forms.HiddenInput()) | ||||
|  | ||||
|     selected_category_templates = forms.BooleanField(required=False, | ||||
|                                                      initial=False, | ||||
|                                                      label=_('Include category parameter templates'), | ||||
|                                                      widget=forms.HiddenInput()) | ||||
|  | ||||
|     parent_category_templates = forms.BooleanField(required=False, | ||||
|                                                    initial=False, | ||||
|                                                    label=_('Include parent categories parameter templates'), | ||||
|                                                    widget=forms.HiddenInput()) | ||||
|  | ||||
|     initial_stock = forms.IntegerField(required=False, | ||||
|                                        initial=0, | ||||
|                                        label=_('Initial stock amount'), | ||||
|                                        help_text=_('Create stock for this part')) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Part | ||||
|         fields = [ | ||||
|             'confirm_creation', | ||||
|             'category', | ||||
|             'selected_category_templates', | ||||
|             'parent_category_templates', | ||||
|             'name', | ||||
|             'IPN', | ||||
|             'description', | ||||
|             'revision', | ||||
|             'bom_copy', | ||||
|             'parameters_copy', | ||||
|             'keywords', | ||||
|             'variant_of', | ||||
|             'link', | ||||
|             'default_location', | ||||
|             'default_supplier', | ||||
|             'default_expiry', | ||||
|             'units', | ||||
|             'minimum_stock', | ||||
|             'initial_stock', | ||||
|             'component', | ||||
|             'assembly', | ||||
|             'is_template', | ||||
|             'trackable', | ||||
|             'purchaseable', | ||||
|             'salable', | ||||
|             'virtual', | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class EditPartParameterTemplateForm(HelperForm): | ||||
|     """ Form for editing a PartParameterTemplate object """ | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,6 @@ from stdimage.models import StdImageField | ||||
|  | ||||
| from decimal import Decimal, InvalidOperation | ||||
| from datetime import datetime | ||||
| from rapidfuzz import fuzz | ||||
| import hashlib | ||||
|  | ||||
| from InvenTree import helpers | ||||
| @@ -235,57 +234,6 @@ def rename_part_image(instance, filename): | ||||
|     return os.path.join(base, fname) | ||||
|  | ||||
|  | ||||
| def match_part_names(match, threshold=80, reverse=True, compare_length=False): | ||||
|     """ Return a list of parts whose name matches the search term using fuzzy search. | ||||
|  | ||||
|     Args: | ||||
|         match: Term to match against | ||||
|         threshold: Match percentage that must be exceeded (default = 65) | ||||
|         reverse: Ordering for search results (default = True - highest match is first) | ||||
|         compare_length: Include string length checks | ||||
|  | ||||
|     Returns: | ||||
|         A sorted dict where each element contains the following key:value pairs: | ||||
|             - 'part' : The matched part | ||||
|             - 'ratio' : The matched ratio | ||||
|     """ | ||||
|  | ||||
|     match = str(match).strip().lower() | ||||
|  | ||||
|     if len(match) == 0: | ||||
|         return [] | ||||
|  | ||||
|     parts = Part.objects.all() | ||||
|  | ||||
|     matches = [] | ||||
|  | ||||
|     for part in parts: | ||||
|         compare = str(part.name).strip().lower() | ||||
|  | ||||
|         if len(compare) == 0: | ||||
|             continue | ||||
|  | ||||
|         ratio = fuzz.partial_token_sort_ratio(compare, match) | ||||
|  | ||||
|         if compare_length: | ||||
|             # Also employ primitive length comparison | ||||
|             # TODO - Improve this somewhat... | ||||
|             l_min = min(len(match), len(compare)) | ||||
|             l_max = max(len(match), len(compare)) | ||||
|  | ||||
|             ratio *= (l_min / l_max) | ||||
|  | ||||
|         if ratio >= threshold: | ||||
|             matches.append({ | ||||
|                 'part': part, | ||||
|                 'ratio': round(ratio, 1) | ||||
|             }) | ||||
|  | ||||
|     matches = sorted(matches, key=lambda item: item['ratio'], reverse=reverse) | ||||
|  | ||||
|     return matches | ||||
|  | ||||
|  | ||||
| class PartManager(TreeManager): | ||||
|     """ | ||||
|     Defines a custom object manager for the Part model. | ||||
| @@ -409,7 +357,7 @@ class Part(MPTTModel): | ||||
|         """ | ||||
|  | ||||
|         # Get category templates settings | ||||
|         add_category_templates = kwargs.pop('add_category_templates', None) | ||||
|         add_category_templates = kwargs.pop('add_category_templates', False) | ||||
|  | ||||
|         if self.pk: | ||||
|             previous = Part.objects.get(pk=self.pk) | ||||
| @@ -437,39 +385,29 @@ class Part(MPTTModel): | ||||
|             # Get part category | ||||
|             category = self.category | ||||
|  | ||||
|             if category and add_category_templates: | ||||
|                 # Store templates added to part | ||||
|             if category is not None: | ||||
|  | ||||
|                 template_list = [] | ||||
|  | ||||
|                 # Create part parameters for selected category | ||||
|                 category_templates = add_category_templates['main'] | ||||
|                 if category_templates: | ||||
|                 parent_categories = category.get_ancestors(include_self=True) | ||||
|  | ||||
|                 for category in parent_categories: | ||||
|                     for template in category.get_parameter_templates(): | ||||
|                         parameter = PartParameter.create(part=self, | ||||
|                                                          template=template.parameter_template, | ||||
|                                                          data=template.default_value, | ||||
|                                                          save=True) | ||||
|                         if parameter: | ||||
|                         # Check that template wasn't already added | ||||
|                         if template.parameter_template not in template_list: | ||||
|  | ||||
|                             template_list.append(template.parameter_template) | ||||
|  | ||||
|                 # Create part parameters for parent category | ||||
|                 category_templates = add_category_templates['parent'] | ||||
|                 if category_templates: | ||||
|                     # Get parent categories | ||||
|                     parent_categories = category.get_ancestors() | ||||
|  | ||||
|                     for category in parent_categories: | ||||
|                         for template in category.get_parameter_templates(): | ||||
|                             # Check that template wasn't already added | ||||
|                             if template.parameter_template not in template_list: | ||||
|                                 try: | ||||
|                                     PartParameter.create(part=self, | ||||
|                                                          template=template.parameter_template, | ||||
|                                                          data=template.default_value, | ||||
|                                                          save=True) | ||||
|                                 except IntegrityError: | ||||
|                                     # PartParameter already exists | ||||
|                                     pass | ||||
|                             try: | ||||
|                                 PartParameter.create( | ||||
|                                     part=self, | ||||
|                                     template=template.parameter_template, | ||||
|                                     data=template.default_value, | ||||
|                                     save=True | ||||
|                                 ) | ||||
|                             except IntegrityError: | ||||
|                                 # PartParameter already exists | ||||
|                                 pass | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"{self.full_name} - {self.description}" | ||||
|   | ||||
| @@ -264,25 +264,25 @@ | ||||
|  | ||||
|     {% if roles.part.add %} | ||||
|     $("#part-create").click(function() { | ||||
|         launchModalForm( | ||||
|             "{% url 'part-create' %}", | ||||
|             { | ||||
|                 follow: true, | ||||
|                 data: { | ||||
|                 {% if category %} | ||||
|                     category: {{ category.id }} | ||||
|                 {% endif %} | ||||
|                 }, | ||||
|                 secondary: [ | ||||
|                     { | ||||
|                         field: 'default_location', | ||||
|                         label: '{% trans "New Location" %}', | ||||
|                         title: '{% trans "Create new Stock Location" %}', | ||||
|                         url: "{% url 'stock-location-create' %}", | ||||
|                     } | ||||
|                 ]    | ||||
|             } | ||||
|         ); | ||||
|  | ||||
|         var fields = partFields({ | ||||
|             create: true, | ||||
|         }); | ||||
|  | ||||
|         {% if category %} | ||||
|         fields.category.value = {{ category.pk }}; | ||||
|         {% endif %} | ||||
|  | ||||
|         constructForm('{% url "api-part-list" %}', { | ||||
|             method: 'POST', | ||||
|             fields: fields, | ||||
|             title: '{% trans "Create Part" %}', | ||||
|             onSuccess: function(data) { | ||||
|                 // Follow the new part | ||||
|                 location.href = `/part/${data.pk}/`; | ||||
|             }, | ||||
|         }); | ||||
|          | ||||
|     }); | ||||
|     {% endif %} | ||||
|  | ||||
|   | ||||
| @@ -525,10 +525,11 @@ | ||||
|     loadPartVariantTable($('#variants-table'), {{ part.pk }}); | ||||
|  | ||||
|     $('#new-variant').click(function() { | ||||
|         launchModalForm( | ||||
|             "{% url 'make-part-variant' part.id %}", | ||||
|  | ||||
|         duplicatePart( | ||||
|             {{ part.pk}}, | ||||
|             { | ||||
|                 follow: true, | ||||
|                 variant: true, | ||||
|             } | ||||
|         ); | ||||
|     }); | ||||
|   | ||||
| @@ -486,12 +486,7 @@ | ||||
|  | ||||
|     {% if roles.part.add %} | ||||
|     $("#part-duplicate").click(function() { | ||||
|         launchModalForm( | ||||
|             "{% url 'part-duplicate' part.id %}", | ||||
|             { | ||||
|                 follow: true, | ||||
|             } | ||||
|         ); | ||||
|         duplicatePart({{ part.pk }}); | ||||
|     }); | ||||
|     {% endif %} | ||||
|  | ||||
|   | ||||
| @@ -434,8 +434,8 @@ class PartAPITest(InvenTreeAPITestCase): | ||||
|         self.assertTrue(data['active']) | ||||
|         self.assertFalse(data['virtual']) | ||||
|  | ||||
|         # By default, parts are not purchaseable | ||||
|         self.assertFalse(data['purchaseable']) | ||||
|         # By default, parts are purchaseable | ||||
|         self.assertTrue(data['purchaseable']) | ||||
|  | ||||
|         # Set the default 'purchaseable' status to True | ||||
|         InvenTreeSetting.set_setting( | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError | ||||
| import os | ||||
|  | ||||
| from .models import Part, PartCategory, PartTestTemplate | ||||
| from .models import rename_part_image, match_part_names | ||||
| from .models import rename_part_image | ||||
| from .templatetags import inventree_extras | ||||
|  | ||||
| import part.settings | ||||
| @@ -163,12 +163,6 @@ class PartTest(TestCase): | ||||
|     def test_copy(self): | ||||
|         self.r2.deep_copy(self.r1, image=True, bom=True) | ||||
|  | ||||
|     def test_match_names(self): | ||||
|  | ||||
|         matches = match_part_names('M2x5 LPHS') | ||||
|  | ||||
|         self.assertTrue(len(matches) > 0) | ||||
|  | ||||
|     def test_sell_pricing(self): | ||||
|         # check that the sell pricebreaks were loaded | ||||
|         self.assertTrue(self.r1.has_price_breaks) | ||||
| @@ -281,7 +275,7 @@ class PartSettingsTest(TestCase): | ||||
|         """ | ||||
|  | ||||
|         self.assertTrue(part.settings.part_component_default()) | ||||
|         self.assertFalse(part.settings.part_purchaseable_default()) | ||||
|         self.assertTrue(part.settings.part_purchaseable_default()) | ||||
|         self.assertFalse(part.settings.part_salable_default()) | ||||
|         self.assertFalse(part.settings.part_trackable_default()) | ||||
|  | ||||
| @@ -293,7 +287,7 @@ class PartSettingsTest(TestCase): | ||||
|         part = self.make_part() | ||||
|  | ||||
|         self.assertTrue(part.component) | ||||
|         self.assertFalse(part.purchaseable) | ||||
|         self.assertTrue(part.purchaseable) | ||||
|         self.assertFalse(part.salable) | ||||
|         self.assertFalse(part.trackable) | ||||
|  | ||||
|   | ||||
| @@ -155,38 +155,6 @@ class PartDetailTest(PartViewTestCase): | ||||
|         self.assertIn('streaming_content', dir(response)) | ||||
|  | ||||
|  | ||||
| class PartTests(PartViewTestCase): | ||||
|     """ Tests for Part forms """ | ||||
|  | ||||
|     def test_part_create(self): | ||||
|         """ Launch form to create a new part """ | ||||
|         response = self.client.get(reverse('part-create'), {'category': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|         # And again, with an invalid category | ||||
|         response = self.client.get(reverse('part-create'), {'category': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|         # And again, with no category | ||||
|         response = self.client.get(reverse('part-create'), {'name': 'Test part'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_part_duplicate(self): | ||||
|         """ Launch form to duplicate part """ | ||||
|  | ||||
|         # First try with an invalid part | ||||
|         response = self.client.get(reverse('part-duplicate', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|         response = self.client.get(reverse('part-duplicate', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_make_variant(self): | ||||
|  | ||||
|         response = self.client.get(reverse('make-part-variant', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|  | ||||
| class PartRelatedTests(PartViewTestCase): | ||||
|  | ||||
|     def test_valid_create(self): | ||||
|   | ||||
| @@ -40,8 +40,7 @@ part_detail_urls = [ | ||||
|     url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'), | ||||
|     url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'), | ||||
|     url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'), | ||||
|     url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'), | ||||
|     url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'), | ||||
|      | ||||
|     url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), | ||||
|  | ||||
|     url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), | ||||
| @@ -81,9 +80,6 @@ category_urls = [ | ||||
| # URL list for part web interface | ||||
| part_urls = [ | ||||
|  | ||||
|     # Create a new part | ||||
|     url(r'^new/?', views.PartCreate.as_view(), name='part-create'), | ||||
|  | ||||
|     # Upload a part | ||||
|     url(r'^import/', views.PartImport.as_view(), name='part-import'), | ||||
|     url(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'), | ||||
|   | ||||
| @@ -14,8 +14,7 @@ from django.shortcuts import HttpResponseRedirect | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.urls import reverse | ||||
| from django.views.generic import DetailView, ListView | ||||
| from django.forms.models import model_to_dict | ||||
| from django.forms import HiddenInput, CheckboxInput | ||||
| from django.forms import HiddenInput | ||||
| from django.conf import settings | ||||
| from django.contrib import messages | ||||
|  | ||||
| @@ -35,7 +34,6 @@ from .models import PartCategory, Part, PartRelated | ||||
| from .models import PartParameterTemplate | ||||
| from .models import PartCategoryParameterTemplate | ||||
| from .models import BomItem | ||||
| from .models import match_part_names | ||||
| from .models import PartSellPriceBreak, PartInternalPriceBreak | ||||
|  | ||||
| from common.models import InvenTreeSetting | ||||
| @@ -44,7 +42,7 @@ from common.files import FileManager | ||||
| from common.views import FileManagementFormView, FileManagementAjaxView | ||||
| from common.forms import UploadFileForm, MatchFieldForm | ||||
|  | ||||
| from stock.models import StockItem, StockLocation | ||||
| from stock.models import StockLocation | ||||
|  | ||||
| import common.settings as inventree_settings | ||||
|  | ||||
| @@ -233,370 +231,6 @@ class PartSetCategory(AjaxUpdateView): | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class MakePartVariant(AjaxCreateView): | ||||
|     """ View for creating a new variant based on an existing template Part | ||||
|  | ||||
|     - Part <pk> is provided in the URL '/part/<pk>/make_variant/' | ||||
|     - Automatically copy relevent data (BOM, etc, etc) | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     model = Part | ||||
|     form_class = part_forms.EditPartForm | ||||
|  | ||||
|     ajax_form_title = _('Create Variant') | ||||
|     ajax_template_name = 'part/variant_part.html' | ||||
|  | ||||
|     def get_part_template(self): | ||||
|         return get_object_or_404(Part, id=self.kwargs['pk']) | ||||
|  | ||||
|     def get_context_data(self): | ||||
|         return { | ||||
|             'part': self.get_part_template(), | ||||
|         } | ||||
|  | ||||
|     def get_form(self): | ||||
|         form = super(AjaxCreateView, self).get_form() | ||||
|  | ||||
|         # Hide some variant-related fields | ||||
|         # form.fields['variant_of'].widget = HiddenInput() | ||||
|  | ||||
|         # Force display of the 'bom_copy' widget | ||||
|         form.fields['bom_copy'].widget = CheckboxInput() | ||||
|  | ||||
|         # Force display of the 'parameters_copy' widget | ||||
|         form.fields['parameters_copy'].widget = CheckboxInput() | ||||
|  | ||||
|         return form | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|  | ||||
|         form = self.get_form() | ||||
|         context = self.get_context_data() | ||||
|         part_template = self.get_part_template() | ||||
|  | ||||
|         valid = form.is_valid() | ||||
|  | ||||
|         data = { | ||||
|             'form_valid': valid, | ||||
|         } | ||||
|  | ||||
|         if valid: | ||||
|             # Create the new part variant | ||||
|             part = form.save(commit=False) | ||||
|             part.variant_of = part_template | ||||
|             part.is_template = False | ||||
|  | ||||
|             part.save() | ||||
|  | ||||
|             data['pk'] = part.pk | ||||
|             data['text'] = str(part) | ||||
|             data['url'] = part.get_absolute_url() | ||||
|  | ||||
|             bom_copy = str2bool(request.POST.get('bom_copy', False)) | ||||
|             parameters_copy = str2bool(request.POST.get('parameters_copy', False)) | ||||
|  | ||||
|             # Copy relevent information from the template part | ||||
|             part.deep_copy(part_template, bom=bom_copy, parameters=parameters_copy) | ||||
|  | ||||
|         return self.renderJsonResponse(request, form, data, context=context) | ||||
|  | ||||
|     def get_initial(self): | ||||
|  | ||||
|         part_template = self.get_part_template() | ||||
|  | ||||
|         initials = model_to_dict(part_template) | ||||
|         initials['is_template'] = False | ||||
|         initials['variant_of'] = part_template | ||||
|         initials['bom_copy'] = InvenTreeSetting.get_setting('PART_COPY_BOM') | ||||
|         initials['parameters_copy'] = InvenTreeSetting.get_setting('PART_COPY_PARAMETERS') | ||||
|  | ||||
|         return initials | ||||
|  | ||||
|  | ||||
| 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, ValueError): | ||||
|             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 'bom_copy' widget | ||||
|         form.fields['bom_copy'].widget = CheckboxInput() | ||||
|  | ||||
|         # Force display of the 'parameters_copy' widget | ||||
|         form.fields['parameters_copy'].widget = CheckboxInput() | ||||
|  | ||||
|         return form | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """ Capture the POST request for part duplication | ||||
|  | ||||
|         - If the bom_copy object is set, copy all the BOM items too! | ||||
|         - If the parameters_copy object is set, copy all the parameters 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: | ||||
|                 # Display the first five closest matches | ||||
|                 context['matches'] = matches[:5] | ||||
|  | ||||
|                 # 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: | ||||
|                     msg = _('Possible matches exist - confirm creation of new part') | ||||
|                     form.add_error('confirm_creation', msg) | ||||
|                     form.pre_form_warning = msg | ||||
|                     valid = False | ||||
|  | ||||
|         data = { | ||||
|             'form_valid': valid | ||||
|         } | ||||
|  | ||||
|         if valid: | ||||
|             # Create the new Part | ||||
|             part = form.save(commit=False) | ||||
|  | ||||
|             part.creation_user = request.user | ||||
|             part.save() | ||||
|  | ||||
|             data['pk'] = part.pk | ||||
|             data['text'] = str(part) | ||||
|  | ||||
|             bom_copy = str2bool(request.POST.get('bom_copy', False)) | ||||
|             parameters_copy = str2bool(request.POST.get('parameters_copy', False)) | ||||
|  | ||||
|             original = self.get_part_to_copy() | ||||
|  | ||||
|             if original: | ||||
|                 part.deep_copy(original, bom=bom_copy, parameters=parameters_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() | ||||
|  | ||||
|         initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True)) | ||||
|  | ||||
|         initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True)) | ||||
|  | ||||
|         return initials | ||||
|  | ||||
|  | ||||
| class PartCreate(AjaxCreateView): | ||||
|     """ View for creating a new Part object. | ||||
|  | ||||
|     Options for providing initial conditions: | ||||
|  | ||||
|     - Provide a category object as initial data | ||||
|     """ | ||||
|     model = Part | ||||
|     form_class = part_forms.EditPartForm | ||||
|  | ||||
|     ajax_form_title = _('Create New Part') | ||||
|     ajax_template_name = 'part/create_part.html' | ||||
|  | ||||
|     def get_data(self): | ||||
|         return { | ||||
|             'success': _("Created new part"), | ||||
|         } | ||||
|  | ||||
|     def get_category_id(self): | ||||
|         return self.request.GET.get('category', None) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """ Provide extra context information for the form to display: | ||||
|  | ||||
|         - Add category information (if provided) | ||||
|         """ | ||||
|         context = super(PartCreate, self).get_context_data(**kwargs) | ||||
|  | ||||
|         # Add category information to the page | ||||
|         cat_id = self.get_category_id() | ||||
|  | ||||
|         if cat_id: | ||||
|             try: | ||||
|                 context['category'] = PartCategory.objects.get(pk=cat_id) | ||||
|             except (PartCategory.DoesNotExist, ValueError): | ||||
|                 pass | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     def get_form(self): | ||||
|         """ Create Form for making new Part object. | ||||
|         Remove the 'default_supplier' field as there are not yet any matching SupplierPart objects | ||||
|         """ | ||||
|         form = super(AjaxCreateView, self).get_form() | ||||
|  | ||||
|         # Hide the "default expiry" field if the feature is not enabled | ||||
|         if not inventree_settings.stock_expiry_enabled(): | ||||
|             form.fields['default_expiry'].widget = HiddenInput() | ||||
|  | ||||
|         # Hide the "initial stock amount" field if the feature is not enabled | ||||
|         if not InvenTreeSetting.get_setting('PART_CREATE_INITIAL'): | ||||
|             form.fields['initial_stock'].widget = HiddenInput() | ||||
|  | ||||
|         # Hide the default_supplier field (there are no matching supplier parts yet!) | ||||
|         form.fields['default_supplier'].widget = HiddenInput() | ||||
|  | ||||
|         # Display category templates widgets | ||||
|         form.fields['selected_category_templates'].widget = CheckboxInput() | ||||
|         form.fields['parent_category_templates'].widget = CheckboxInput() | ||||
|  | ||||
|         return form | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|  | ||||
|         form = self.get_form() | ||||
|  | ||||
|         context = {} | ||||
|  | ||||
|         valid = form.is_valid() | ||||
|  | ||||
|         name = request.POST.get('name', None) | ||||
|  | ||||
|         if name: | ||||
|             matches = match_part_names(name) | ||||
|  | ||||
|             if len(matches) > 0: | ||||
|  | ||||
|                 # Limit to the top 5 matches (to prevent clutter) | ||||
|                 context['matches'] = matches[:5] | ||||
|  | ||||
|                 # 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: | ||||
|                     msg = _('Possible matches exist - confirm creation of new part') | ||||
|                     form.add_error('confirm_creation', msg) | ||||
|  | ||||
|                     form.pre_form_warning = msg | ||||
|                     valid = False | ||||
|  | ||||
|         data = { | ||||
|             'form_valid': valid | ||||
|         } | ||||
|  | ||||
|         if valid: | ||||
|             # Create the new Part | ||||
|             part = form.save(commit=False) | ||||
|  | ||||
|             # Record the user who created this part | ||||
|             part.creation_user = request.user | ||||
|  | ||||
|             # Store category templates settings | ||||
|             add_category_templates = { | ||||
|                 'main': form.cleaned_data['selected_category_templates'], | ||||
|                 'parent': form.cleaned_data['parent_category_templates'], | ||||
|             } | ||||
|  | ||||
|             # Save part and pass category template settings | ||||
|             part.save(**{'add_category_templates': add_category_templates}) | ||||
|  | ||||
|             # Add stock if set | ||||
|             init_stock = int(request.POST.get('initial_stock', 0)) | ||||
|             if init_stock: | ||||
|                 stock = StockItem(part=part, | ||||
|                                   quantity=init_stock, | ||||
|                                   location=part.default_location) | ||||
|                 stock.save() | ||||
|  | ||||
|             data['pk'] = part.pk | ||||
|             data['text'] = str(part) | ||||
|  | ||||
|             try: | ||||
|                 data['url'] = part.get_absolute_url() | ||||
|             except AttributeError: | ||||
|                 pass | ||||
|  | ||||
|         return self.renderJsonResponse(request, form, data, context=context) | ||||
|  | ||||
|     def get_initial(self): | ||||
|         """ Get initial data for the new Part object: | ||||
|  | ||||
|         - If a category is provided, pre-fill the Category field | ||||
|         """ | ||||
|  | ||||
|         initials = super(PartCreate, self).get_initial() | ||||
|  | ||||
|         if self.get_category_id(): | ||||
|             try: | ||||
|                 category = PartCategory.objects.get(pk=self.get_category_id()) | ||||
|                 initials['category'] = category | ||||
|                 initials['keywords'] = category.default_keywords | ||||
|             except (PartCategory.DoesNotExist, ValueError): | ||||
|                 pass | ||||
|  | ||||
|         # Allow initial data to be passed through as arguments | ||||
|         for label in ['name', 'IPN', 'description', 'revision', 'keywords']: | ||||
|             if label in self.request.GET: | ||||
|                 initials[label] = self.request.GET.get(label) | ||||
|  | ||||
|         # Automatically create part parameters from category templates | ||||
|         initials['selected_category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False)) | ||||
|         initials['parent_category_templates'] = initials['selected_category_templates'] | ||||
|  | ||||
|         return initials | ||||
|  | ||||
|  | ||||
| class PartImport(FileManagementFormView): | ||||
|     ''' Part: Upload file, match to fields and import parts(using multi-Step form) ''' | ||||
|     permission_required = 'part.add' | ||||
|   | ||||
| @@ -240,6 +240,7 @@ function constructDeleteForm(fields, options) { | ||||
|  *      - hidden: Set to true to hide the field | ||||
|  *      - icon: font-awesome icon to display before the field | ||||
|  *      - prefix: Custom HTML prefix to display before the field | ||||
|  * - data: map of data to fill out field values with | ||||
|  * - focus: Name of field to focus on when modal is displayed | ||||
|  * - preventClose: Set to true to prevent form from closing on success | ||||
|  * - onSuccess: callback function when form action is successful | ||||
| @@ -263,6 +264,11 @@ function constructForm(url, options) { | ||||
|     // Default HTTP method | ||||
|     options.method = options.method || 'PATCH'; | ||||
|  | ||||
|     // Construct an "empty" data object if not provided | ||||
|     if (!options.data) { | ||||
|         options.data = {}; | ||||
|     } | ||||
|  | ||||
|     // Request OPTIONS endpoint from the API | ||||
|     getApiEndpointOptions(url, function(OPTIONS) { | ||||
|  | ||||
| @@ -346,10 +352,19 @@ function constructFormBody(fields, options) { | ||||
|     // otherwise *all* fields will be displayed | ||||
|     var displayed_fields = options.fields || fields; | ||||
|  | ||||
|     // Handle initial data overrides | ||||
|     if (options.data) { | ||||
|         for (const field in options.data) { | ||||
|  | ||||
|             if (field in fields) { | ||||
|                 fields[field].value = options.data[field]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Provide each field object with its own name | ||||
|     for(field in fields) { | ||||
|         fields[field].name = field; | ||||
|  | ||||
|          | ||||
|         // If any "instance_filters" are defined for the endpoint, copy them across (overwrite) | ||||
|         if (fields[field].instance_filters) { | ||||
| @@ -366,6 +381,10 @@ function constructFormBody(fields, options) { | ||||
|  | ||||
|             // TODO: Refactor the following code with Object.assign (see above) | ||||
|  | ||||
|             // "before" and "after" renders | ||||
|             fields[field].before = field_options.before; | ||||
|             fields[field].after = field_options.after; | ||||
|  | ||||
|             // Secondary modal options | ||||
|             fields[field].secondary = field_options.secondary; | ||||
|  | ||||
| @@ -560,10 +579,15 @@ function submitFormData(fields, options) { | ||||
|     var has_files = false; | ||||
|  | ||||
|     // Extract values for each field | ||||
|     options.field_names.forEach(function(name) { | ||||
|     for (var idx = 0; idx < options.field_names.length; idx++) { | ||||
|  | ||||
|         var name = options.field_names[idx]; | ||||
|  | ||||
|         var field = fields[name] || null; | ||||
|  | ||||
|         // Ignore visual fields | ||||
|         if (field && field.type == 'candy') continue; | ||||
|  | ||||
|         if (field) { | ||||
|  | ||||
|             var value = getFormFieldValue(name, field, options); | ||||
| @@ -593,7 +617,7 @@ function submitFormData(fields, options) { | ||||
|         } else { | ||||
|             console.log(`WARNING: Could not find field matching '${name}'`); | ||||
|         } | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     var upload_func = inventreePut; | ||||
|  | ||||
| @@ -1279,6 +1303,11 @@ function renderModelData(name, model, data, parameters, options) { | ||||
|  */ | ||||
| function constructField(name, parameters, options) { | ||||
|  | ||||
|     // Shortcut for simple visual fields | ||||
|     if (parameters.type == 'candy') { | ||||
|         return constructCandyInput(name, parameters, options); | ||||
|     } | ||||
|  | ||||
|     var field_name = `id_${name}`; | ||||
|  | ||||
|     // Hidden inputs are rendered without label / help text / etc | ||||
| @@ -1292,7 +1321,14 @@ function constructField(name, parameters, options) { | ||||
|         form_classes += ' has-error'; | ||||
|     } | ||||
|  | ||||
|     var html = `<div id='div_${field_name}' class='${form_classes}'>`; | ||||
|     var html = ''; | ||||
|      | ||||
|     // Optional content to render before the field | ||||
|     if (parameters.before) { | ||||
|         html += parameters.before; | ||||
|     } | ||||
|      | ||||
|     html += `<div id='div_${field_name}' class='${form_classes}'>`; | ||||
|  | ||||
|     // Add a label | ||||
|     html += constructLabel(name, parameters); | ||||
| @@ -1352,6 +1388,10 @@ function constructField(name, parameters, options) { | ||||
|     html += `</div>`;   // controls | ||||
|     html += `</div>`;   // form-group | ||||
|      | ||||
|     if (parameters.after) { | ||||
|         html += parameters.after; | ||||
|     } | ||||
|  | ||||
|     return html; | ||||
| } | ||||
|  | ||||
| @@ -1430,6 +1470,9 @@ function constructInput(name, parameters, options) { | ||||
|         case 'date': | ||||
|             func = constructDateInput; | ||||
|             break; | ||||
|         case 'candy': | ||||
|             func = constructCandyInput; | ||||
|             break; | ||||
|         default: | ||||
|             // Unsupported field type! | ||||
|             break; | ||||
| @@ -1658,6 +1701,17 @@ function constructDateInput(name, parameters, options) { | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Construct a "candy" field input | ||||
|  * No actual field data! | ||||
|  */ | ||||
| function constructCandyInput(name, parameters, options) { | ||||
|  | ||||
|     return parameters.html; | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Construct a 'help text' div based on the field parameters | ||||
|  *  | ||||
|   | ||||
| @@ -13,6 +13,134 @@ function yesNoLabel(value) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Construct fieldset for part forms | ||||
| function partFields(options={}) { | ||||
|  | ||||
|     var fields = { | ||||
|         category: {}, | ||||
|         name: {}, | ||||
|         IPN: {}, | ||||
|         revision: {}, | ||||
|         description: {}, | ||||
|         variant_of: {}, | ||||
|         keywords: { | ||||
|             icon: 'fa-key', | ||||
|         }, | ||||
|         units: {}, | ||||
|         link: { | ||||
|             icon: 'fa-link', | ||||
|         }, | ||||
|         default_location: {}, | ||||
|         default_supplier: {}, | ||||
|         default_expiry: { | ||||
|             icon: 'fa-calendar-alt', | ||||
|         }, | ||||
|         minimum_stock: { | ||||
|             icon: 'fa-boxes', | ||||
|         }, | ||||
|         attributes: { | ||||
|             type: 'candy', | ||||
|             html: `<hr><h4><i>{% trans "Part Attributes" %}</i></h4><hr>` | ||||
|         }, | ||||
|         component: { | ||||
|             value: global_settings.PART_COMPONENT, | ||||
|         }, | ||||
|         assembly: { | ||||
|             value: global_settings.PART_ASSEMBLY, | ||||
|         }, | ||||
|         is_template: { | ||||
|             value: global_settings.PART_TEMPLATE, | ||||
|         }, | ||||
|         trackable: { | ||||
|             value: global_settings.PART_TRACKABLE, | ||||
|         }, | ||||
|         purchaseable: { | ||||
|             value: global_settings.PART_PURCHASEABLE, | ||||
|         }, | ||||
|         salable: { | ||||
|             value: global_settings.PART_SALABLE, | ||||
|         }, | ||||
|         virtual: { | ||||
|             value: global_settings.PART_VIRTUAL, | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     // If editing a part, we can set the "active" status | ||||
|     if (options.edit) { | ||||
|         fields.active = {}; | ||||
|     } | ||||
|  | ||||
|     // Pop expiry field | ||||
|     if (!global_settings.STOCK_ENABLE_EXPIRY) { | ||||
|         delete fields["default_expiry"]; | ||||
|     } | ||||
|  | ||||
|     // Additional fields when "creating" a new part | ||||
|     if (options.create) { | ||||
|  | ||||
|         // No supplier parts available yet | ||||
|         delete fields["default_supplier"]; | ||||
|  | ||||
|         fields.create = { | ||||
|             type: 'candy', | ||||
|             html: `<hr><h4><i>{% trans "Part Creation Options" %}</i></h4><hr>`, | ||||
|         }; | ||||
|  | ||||
|         if (global_settings.PART_CREATE_INITIAL) { | ||||
|             fields.initial_stock = { | ||||
|                 type: 'decimal', | ||||
|                 label: '{% trans "Initial Stock Quantity" %}', | ||||
|                 help_text: '{% trans "Initialize part stock with specified quantity" %}', | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         fields.copy_category_parameters = { | ||||
|             type: 'boolean', | ||||
|             label: '{% trans "Copy Category Parameters" %}', | ||||
|             help_text: '{% trans "Copy parameter templates from selected part category" %}', | ||||
|             value: global_settings.PART_CATEGORY_PARAMETERS, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // Additional fields when "duplicating" a part | ||||
|     if (options.duplicate) { | ||||
|  | ||||
|         fields.duplicate = { | ||||
|             type: 'candy', | ||||
|             html: `<hr><h4><i>{% trans "Part Duplication Options" %}</i></h4><hr>`, | ||||
|         }; | ||||
|  | ||||
|         fields.copy_from = { | ||||
|             type: 'integer', | ||||
|             hidden: true, | ||||
|             value: options.duplicate, | ||||
|         }, | ||||
|  | ||||
|         fields.copy_image = { | ||||
|             type: 'boolean', | ||||
|             label: '{% trans "Copy Image" %}', | ||||
|             help_text: '{% trans "Copy image from original part" %}', | ||||
|             value: true, | ||||
|         }, | ||||
|  | ||||
|         fields.copy_bom = { | ||||
|             type: 'boolean', | ||||
|             label: '{% trans "Copy BOM" %}', | ||||
|             help_text: '{% trans "Copy bill of materials from original part" %}', | ||||
|             value: global_settings.PART_COPY_BOM, | ||||
|         }; | ||||
|  | ||||
|         fields.copy_parameters = { | ||||
|             type: 'boolean', | ||||
|             label: '{% trans "Copy Parameters" %}', | ||||
|             help_text: '{% trans "Copy parameter data from original part" %}', | ||||
|             value: global_settings.PART_COPY_PARAMETERS, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     return fields; | ||||
| } | ||||
|  | ||||
|  | ||||
| function categoryFields() { | ||||
|     return { | ||||
| @@ -49,86 +177,49 @@ function editPart(pk, options={}) { | ||||
|  | ||||
|     var url = `/api/part/${pk}/`; | ||||
|  | ||||
|     var fields =  { | ||||
|         category: { | ||||
|             /* | ||||
|             secondary: { | ||||
|                 label: '{% trans "New Category" %}', | ||||
|                 title: '{% trans "Create New Part Category" %}', | ||||
|                 api_url: '{% url "api-part-category-list" %}', | ||||
|                 method: 'POST', | ||||
|                 fields: { | ||||
|                     name: {}, | ||||
|                     description: {}, | ||||
|                     parent: { | ||||
|                         secondary: { | ||||
|                             title: '{% trans "New Parent" %}', | ||||
|                             api_url: '{% url "api-part-category-list" %}', | ||||
|                             method: 'POST', | ||||
|                             fields: { | ||||
|                                 name: {}, | ||||
|                                 description: {}, | ||||
|                                 parent: {}, | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                 } | ||||
|             }, | ||||
|             */ | ||||
|         }, | ||||
|         name: { | ||||
|             placeholder: 'part name', | ||||
|         }, | ||||
|         IPN: {}, | ||||
|         description: {}, | ||||
|         revision: {}, | ||||
|         keywords: { | ||||
|             icon: 'fa-key', | ||||
|         }, | ||||
|         variant_of: {}, | ||||
|         link: { | ||||
|             icon: 'fa-link', | ||||
|         }, | ||||
|         default_location: { | ||||
|             /* | ||||
|             secondary: { | ||||
|                 label: '{% trans "New Location" %}', | ||||
|                 title: '{% trans "Create new stock location" %}', | ||||
|             }, | ||||
|             */ | ||||
|         }, | ||||
|         default_supplier: { | ||||
|             filters: { | ||||
|                 part: pk, | ||||
|                 part_detail: true, | ||||
|                 manufacturer_detail: true, | ||||
|                 supplier_detail: true, | ||||
|             }, | ||||
|             /* | ||||
|             secondary: { | ||||
|                 label: '{% trans "New Supplier Part" %}', | ||||
|                 title: '{% trans "Create new supplier part" %}', | ||||
|             } | ||||
|             */ | ||||
|         }, | ||||
|         units: {}, | ||||
|         minimum_stock: {}, | ||||
|         virtual: {}, | ||||
|         is_template: {}, | ||||
|         assembly: {}, | ||||
|         component: {}, | ||||
|         trackable: {}, | ||||
|         purchaseable: {}, | ||||
|         salable: {}, | ||||
|         active: {}, | ||||
|     }; | ||||
|     var fields = partFields({ | ||||
|         edit: true | ||||
|     }); | ||||
|  | ||||
|     constructForm(url, { | ||||
|         fields: fields, | ||||
|         title: '{% trans "Edit Part" %}', | ||||
|         reload: true, | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| // Launch form to duplicate a part | ||||
| function duplicatePart(pk, options={}) { | ||||
|  | ||||
|     // First we need all the part information | ||||
|     inventreeGet(`/api/part/${pk}/`, {}, { | ||||
|  | ||||
|         success: function(data) { | ||||
|              | ||||
|             var fields = partFields({ | ||||
|                 duplicate: pk, | ||||
|             }); | ||||
|  | ||||
|             // If we are making a "variant" part | ||||
|             if (options.variant) { | ||||
|  | ||||
|                 // Override the "variant_of" field | ||||
|                 data.variant_of = pk; | ||||
|             } | ||||
|              | ||||
|             constructForm('{% url "api-part-list" %}', { | ||||
|                 method: 'POST', | ||||
|                 fields: fields, | ||||
|                 title: '{% trans "Duplicate Part" %}', | ||||
|                 data: data, | ||||
|                 onSuccess: function(data) { | ||||
|                     // Follow the new part | ||||
|                     location.href = `/part/${data.pk}/`; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user