mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Merge pull request #1956 from SchrodingersGat/supplier-part-from-form
Supplier part from form
This commit is contained in:
		@@ -730,6 +730,13 @@
 | 
				
			|||||||
  padding: 10px;
 | 
					  padding: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-panel {
 | 
				
			||||||
 | 
					    border-radius: 5px;
 | 
				
			||||||
 | 
					    border: 1px solid #ccc;
 | 
				
			||||||
 | 
					    padding: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal input {
 | 
					.modal input {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,12 +9,14 @@ from django.conf.urls import url, include
 | 
				
			|||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.http import JsonResponse
 | 
					from django.http import JsonResponse
 | 
				
			||||||
from django.db.models import Q, F, Count, Min, Max, Avg
 | 
					from django.db.models import Q, F, Count, Min, Max, Avg
 | 
				
			||||||
 | 
					from django.db import transaction
 | 
				
			||||||
from django.utils.translation import ugettext_lazy as _
 | 
					from django.utils.translation import ugettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rest_framework import status
 | 
					from rest_framework import status
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework import filters, serializers
 | 
					from rest_framework import filters, serializers
 | 
				
			||||||
from rest_framework import generics
 | 
					from rest_framework import generics
 | 
				
			||||||
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
from django_filters import rest_framework as rest_filters
 | 
					from django_filters import rest_framework as rest_filters
 | 
				
			||||||
@@ -23,7 +25,7 @@ from djmoney.money import Money
 | 
				
			|||||||
from djmoney.contrib.exchange.models import convert_money
 | 
					from djmoney.contrib.exchange.models import convert_money
 | 
				
			||||||
from djmoney.contrib.exchange.exceptions import MissingRate
 | 
					from djmoney.contrib.exchange.exceptions import MissingRate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from decimal import Decimal
 | 
					from decimal import Decimal, InvalidOperation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Part, PartCategory, BomItem
 | 
					from .models import Part, PartCategory, BomItem
 | 
				
			||||||
from .models import PartParameter, PartParameterTemplate
 | 
					from .models import PartParameter, PartParameterTemplate
 | 
				
			||||||
@@ -31,7 +33,10 @@ from .models import PartAttachment, PartTestTemplate
 | 
				
			|||||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
 | 
					from .models import PartSellPriceBreak, PartInternalPriceBreak
 | 
				
			||||||
from .models import PartCategoryParameterTemplate
 | 
					from .models import PartCategoryParameterTemplate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from stock.models import StockItem
 | 
					from company.models import Company, ManufacturerPart, SupplierPart
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from stock.models import StockItem, StockLocation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from common.models import InvenTreeSetting
 | 
					from common.models import InvenTreeSetting
 | 
				
			||||||
from build.models import Build
 | 
					from build.models import Build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -630,6 +635,7 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return Response(data)
 | 
					            return Response(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @transaction.atomic
 | 
				
			||||||
    def create(self, request, *args, **kwargs):
 | 
					    def create(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        We wish to save the user who created this part!
 | 
					        We wish to save the user who created this part!
 | 
				
			||||||
@@ -637,6 +643,8 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
        Note: Implementation copied from DRF class CreateModelMixin
 | 
					        Note: Implementation copied from DRF class CreateModelMixin
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: Unit tests for this function!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        serializer = self.get_serializer(data=request.data)
 | 
					        serializer = self.get_serializer(data=request.data)
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -680,21 +688,97 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Optionally create initial stock item
 | 
					        # Optionally create initial stock item
 | 
				
			||||||
        try:
 | 
					        initial_stock = str2bool(request.data.get('initial_stock', False))
 | 
				
			||||||
            initial_stock = Decimal(request.data.get('initial_stock', 0))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if initial_stock > 0 and part.default_location is not None:
 | 
					        if initial_stock:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                initial_stock_quantity = Decimal(request.data.get('initial_stock_quantity', ''))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if initial_stock_quantity <= 0:
 | 
				
			||||||
 | 
					                    raise ValidationError({
 | 
				
			||||||
 | 
					                        'initial_stock_quantity': [_('Must be greater than zero')],
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					            except (ValueError, InvalidOperation):  # Invalid quantity provided
 | 
				
			||||||
 | 
					                raise ValidationError({
 | 
				
			||||||
 | 
					                    'initial_stock_quantity': [_('Must be a valid quantity')],
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            initial_stock_location = request.data.get('initial_stock_location', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                initial_stock_location = StockLocation.objects.get(pk=initial_stock_location)
 | 
				
			||||||
 | 
					            except (ValueError, StockLocation.DoesNotExist):
 | 
				
			||||||
 | 
					                initial_stock_location = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if initial_stock_location is None:
 | 
				
			||||||
 | 
					                if part.default_location is not None:
 | 
				
			||||||
 | 
					                    initial_stock_location = part.default_location
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    raise ValidationError({
 | 
				
			||||||
 | 
					                        'initial_stock_location': [_('Specify location for initial part stock')],
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            stock_item = StockItem(
 | 
					            stock_item = StockItem(
 | 
				
			||||||
                part=part,
 | 
					                part=part,
 | 
				
			||||||
                    quantity=initial_stock,
 | 
					                quantity=initial_stock_quantity,
 | 
				
			||||||
                    location=part.default_location,
 | 
					                location=initial_stock_location,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            stock_item.save(user=request.user)
 | 
					            stock_item.save(user=request.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Optionally add manufacturer / supplier data to the part
 | 
				
			||||||
 | 
					        if part.purchaseable and str2bool(request.data.get('add_supplier_info', False)):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                manufacturer = Company.objects.get(pk=request.data.get('manufacturer', None))
 | 
				
			||||||
            except:
 | 
					            except:
 | 
				
			||||||
            pass
 | 
					                manufacturer = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                supplier = Company.objects.get(pk=request.data.get('supplier', None))
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                supplier = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            mpn = str(request.data.get('MPN', '')).strip()
 | 
				
			||||||
 | 
					            sku = str(request.data.get('SKU', '')).strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Construct a manufacturer part
 | 
				
			||||||
 | 
					            if manufacturer or mpn:
 | 
				
			||||||
 | 
					                if not manufacturer:
 | 
				
			||||||
 | 
					                    raise ValidationError({
 | 
				
			||||||
 | 
					                        'manufacturer': [_("This field is required")]
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                if not mpn:
 | 
				
			||||||
 | 
					                    raise ValidationError({
 | 
				
			||||||
 | 
					                        'MPN': [_("This field is required")]
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                manufacturer_part = ManufacturerPart.objects.create(
 | 
				
			||||||
 | 
					                    part=part,
 | 
				
			||||||
 | 
					                    manufacturer=manufacturer,
 | 
				
			||||||
 | 
					                    MPN=mpn
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # No manufacturer part data specified
 | 
				
			||||||
 | 
					                manufacturer_part = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if supplier or sku:
 | 
				
			||||||
 | 
					                if not supplier:
 | 
				
			||||||
 | 
					                    raise ValidationError({
 | 
				
			||||||
 | 
					                        'supplier': [_("This field is required")]
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                if not sku:
 | 
				
			||||||
 | 
					                    raise ValidationError({
 | 
				
			||||||
 | 
					                        'SKU': [_("This field is required")]
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                SupplierPart.objects.create(
 | 
				
			||||||
 | 
					                    part=part,
 | 
				
			||||||
 | 
					                    supplier=supplier,
 | 
				
			||||||
 | 
					                    SKU=sku,
 | 
				
			||||||
 | 
					                    manufacturer_part=manufacturer_part,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        headers = self.get_success_headers(serializer.data)
 | 
					        headers = self.get_success_headers(serializer.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -276,6 +276,7 @@
 | 
				
			|||||||
        constructForm('{% url "api-part-list" %}', {
 | 
					        constructForm('{% url "api-part-list" %}', {
 | 
				
			||||||
            method: 'POST',
 | 
					            method: 'POST',
 | 
				
			||||||
            fields: fields,
 | 
					            fields: fields,
 | 
				
			||||||
 | 
					            groups: partGroups(),
 | 
				
			||||||
            title: '{% trans "Create Part" %}',
 | 
					            title: '{% trans "Create Part" %}',
 | 
				
			||||||
            onSuccess: function(data) {
 | 
					            onSuccess: function(data) {
 | 
				
			||||||
                // Follow the new part
 | 
					                // Follow the new part
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -129,6 +129,7 @@ class PartAPITest(InvenTreeAPITestCase):
 | 
				
			|||||||
        'location',
 | 
					        'location',
 | 
				
			||||||
        'bom',
 | 
					        'bom',
 | 
				
			||||||
        'test_templates',
 | 
					        'test_templates',
 | 
				
			||||||
 | 
					        'company',
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    roles = [
 | 
					    roles = [
 | 
				
			||||||
@@ -465,6 +466,128 @@ class PartAPITest(InvenTreeAPITestCase):
 | 
				
			|||||||
        self.assertFalse(response.data['active'])
 | 
					        self.assertFalse(response.data['active'])
 | 
				
			||||||
        self.assertFalse(response.data['purchaseable'])
 | 
					        self.assertFalse(response.data['purchaseable'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_initial_stock(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Tests for initial stock quantity creation
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url = reverse('api-part-list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Track how many parts exist at the start of this test
 | 
				
			||||||
 | 
					        n = Part.objects.count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Set up required part data
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            'category': 1,
 | 
				
			||||||
 | 
					            'name': "My lil' test part",
 | 
				
			||||||
 | 
					            'description': 'A part with which to test',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Signal that we want to add initial stock
 | 
				
			||||||
 | 
					        data['initial_stock'] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Post without a quantity
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('initial_stock_quantity', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Post with an invalid quantity
 | 
				
			||||||
 | 
					        data['initial_stock_quantity'] = "ax"
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('initial_stock_quantity', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Post with a negative quantity
 | 
				
			||||||
 | 
					        data['initial_stock_quantity'] = -1
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('Must be greater than zero', response.data['initial_stock_quantity'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Post with a valid quantity
 | 
				
			||||||
 | 
					        data['initial_stock_quantity'] = 12345
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('initial_stock_location', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check that the number of parts has not increased (due to form failures)
 | 
				
			||||||
 | 
					        self.assertEqual(Part.objects.count(), n)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Now, set a location
 | 
				
			||||||
 | 
					        data['initial_stock_location'] = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=201)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check that the part has been created
 | 
				
			||||||
 | 
					        self.assertEqual(Part.objects.count(), n + 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pk = response.data['pk']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new_part = Part.objects.get(pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(new_part.total_stock, 12345)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_initial_supplier_data(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Tests for initial creation of supplier / manufacturer data
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url = reverse('api-part-list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        n = Part.objects.count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Set up initial part data
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            'category': 1,
 | 
				
			||||||
 | 
					            'name': 'Buy Buy Buy',
 | 
				
			||||||
 | 
					            'description': 'A purchaseable part',
 | 
				
			||||||
 | 
					            'purchaseable': True,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Signal that we wish to create initial supplier data
 | 
				
			||||||
 | 
					        data['add_supplier_info'] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Specify MPN but not manufacturer
 | 
				
			||||||
 | 
					        data['MPN'] = 'MPN-123'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('manufacturer', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Specify manufacturer but not MPN
 | 
				
			||||||
 | 
					        del data['MPN']
 | 
				
			||||||
 | 
					        data['manufacturer'] = 1
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('MPN', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Specify SKU but not supplier
 | 
				
			||||||
 | 
					        del data['manufacturer']
 | 
				
			||||||
 | 
					        data['SKU'] = 'SKU-123'
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('supplier', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Specify supplier but not SKU
 | 
				
			||||||
 | 
					        del data['SKU']
 | 
				
			||||||
 | 
					        data['supplier'] = 1
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=400)
 | 
				
			||||||
 | 
					        self.assertIn('SKU', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check that no new parts have been created
 | 
				
			||||||
 | 
					        self.assertEqual(Part.objects.count(), n)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Now, fully specify the details
 | 
				
			||||||
 | 
					        data['SKU'] = 'SKU-123'
 | 
				
			||||||
 | 
					        data['supplier'] = 3
 | 
				
			||||||
 | 
					        data['MPN'] = 'MPN-123'
 | 
				
			||||||
 | 
					        data['manufacturer'] = 6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(url, data, expected_code=201)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(Part.objects.count(), n + 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pk = response.data['pk']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new_part = Part.objects.get(pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check that there is a new manufacturer part *and* a new supplier part
 | 
				
			||||||
 | 
					        self.assertEqual(new_part.supplier_parts.count(), 1)
 | 
				
			||||||
 | 
					        self.assertEqual(new_part.manufacturer_parts.count(), 1)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PartDetailTests(InvenTreeAPITestCase):
 | 
					class PartDetailTests(InvenTreeAPITestCase):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -264,6 +264,10 @@ function constructForm(url, options) {
 | 
				
			|||||||
    // Default HTTP method
 | 
					    // Default HTTP method
 | 
				
			||||||
    options.method = options.method || 'PATCH';
 | 
					    options.method = options.method || 'PATCH';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Default "groups" definition
 | 
				
			||||||
 | 
					    options.groups = options.groups || {};
 | 
				
			||||||
 | 
					    options.current_group = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Construct an "empty" data object if not provided
 | 
					    // Construct an "empty" data object if not provided
 | 
				
			||||||
    if (!options.data) {
 | 
					    if (!options.data) {
 | 
				
			||||||
        options.data = {};
 | 
					        options.data = {};
 | 
				
			||||||
@@ -362,6 +366,14 @@ function constructFormBody(fields, options) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize an "empty" field for each specified field
 | 
				
			||||||
 | 
					    for (field in displayed_fields) {
 | 
				
			||||||
 | 
					        if (!(field in fields)) {
 | 
				
			||||||
 | 
					            console.log("adding blank field for ", field);
 | 
				
			||||||
 | 
					            fields[field] = {};
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Provide each field object with its own name
 | 
					    // Provide each field object with its own name
 | 
				
			||||||
    for(field in fields) {
 | 
					    for(field in fields) {
 | 
				
			||||||
        fields[field].name = field;
 | 
					        fields[field].name = field;
 | 
				
			||||||
@@ -379,52 +391,18 @@ function constructFormBody(fields, options) {
 | 
				
			|||||||
            // Override existing query filters (if provided!)
 | 
					            // Override existing query filters (if provided!)
 | 
				
			||||||
            fields[field].filters = Object.assign(fields[field].filters || {}, field_options.filters);
 | 
					            fields[field].filters = Object.assign(fields[field].filters || {}, field_options.filters);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // TODO: Refactor the following code with Object.assign (see above)
 | 
					            for (var opt in field_options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // "before" and "after" renders
 | 
					                var val = field_options[opt];
 | 
				
			||||||
            fields[field].before = field_options.before;
 | 
					 | 
				
			||||||
            fields[field].after = field_options.after;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Secondary modal options
 | 
					                if (opt == 'filters') {
 | 
				
			||||||
            fields[field].secondary = field_options.secondary;
 | 
					                    // ignore filters (see above)
 | 
				
			||||||
 | 
					                } else if (opt == 'icon') {
 | 
				
			||||||
            // Edit callback
 | 
					                    // Specify custom icon
 | 
				
			||||||
            fields[field].onEdit = field_options.onEdit;
 | 
					                    fields[field].prefix = `<span class='fas ${val}'></span>`;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
            fields[field].multiline = field_options.multiline;
 | 
					                    fields[field][opt] = field_options[opt];
 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Custom help_text
 | 
					 | 
				
			||||||
            if (field_options.help_text) {
 | 
					 | 
				
			||||||
                fields[field].help_text = field_options.help_text;
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Custom label
 | 
					 | 
				
			||||||
            if (field_options.label) {
 | 
					 | 
				
			||||||
                fields[field].label = field_options.label;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Custom placeholder
 | 
					 | 
				
			||||||
            if (field_options.placeholder) {
 | 
					 | 
				
			||||||
                fields[field].placeholder = field_options.placeholder;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Choices
 | 
					 | 
				
			||||||
            if (field_options.choices) {
 | 
					 | 
				
			||||||
                fields[field].choices = field_options.choices;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Field prefix
 | 
					 | 
				
			||||||
            if (field_options.prefix) {
 | 
					 | 
				
			||||||
                fields[field].prefix = field_options.prefix;
 | 
					 | 
				
			||||||
            } else if (field_options.icon) {
 | 
					 | 
				
			||||||
                // Specify icon like 'fa-user'
 | 
					 | 
				
			||||||
                fields[field].prefix = `<span class='fas ${field_options.icon}'></span>`;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fields[field].hidden = field_options.hidden;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (field_options.read_only != null) {
 | 
					 | 
				
			||||||
                fields[field].read_only = field_options.read_only;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -465,8 +443,10 @@ function constructFormBody(fields, options) {
 | 
				
			|||||||
        html += constructField(name, field, options);
 | 
					        html += constructField(name, field, options);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO: Dynamically create the modals,
 | 
					    if (options.current_group) {
 | 
				
			||||||
    //       so that we can have an infinite number of stacks!
 | 
					        // Close out the current group
 | 
				
			||||||
 | 
					        html += `</div></div>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Create a new modal if one does not exists
 | 
					    // Create a new modal if one does not exists
 | 
				
			||||||
    if (!options.modal) {
 | 
					    if (!options.modal) {
 | 
				
			||||||
@@ -535,6 +515,8 @@ function constructFormBody(fields, options) {
 | 
				
			|||||||
            submitFormData(fields, options);
 | 
					            submitFormData(fields, options);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initializeGroups(fields, options);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -860,9 +842,12 @@ function handleFormErrors(errors, fields, options) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    var non_field_errors = $(options.modal).find('#non-field-errors');
 | 
					    var non_field_errors = $(options.modal).find('#non-field-errors');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO: Display the JSON error text when hovering over the "info" icon
 | 
				
			||||||
    non_field_errors.append(
 | 
					    non_field_errors.append(
 | 
				
			||||||
        `<div class='alert alert-block alert-danger'>
 | 
					        `<div class='alert alert-block alert-danger'>
 | 
				
			||||||
            <b>{% trans "Form errors exist" %}</b>
 | 
					            <b>{% trans "Form errors exist" %}</b>
 | 
				
			||||||
 | 
					            <span id='form-errors-info' class='float-right fas fa-info-circle icon-red'>
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
        </div>`
 | 
					        </div>`
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -932,7 +917,10 @@ function addFieldCallbacks(fields, options) {
 | 
				
			|||||||
function addFieldCallback(name, field, options) {
 | 
					function addFieldCallback(name, field, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(options.modal).find(`#id_${name}`).change(function() {
 | 
					    $(options.modal).find(`#id_${name}`).change(function() {
 | 
				
			||||||
        field.onEdit(name, field, options);
 | 
					
 | 
				
			||||||
 | 
					        var value = getFormFieldValue(name, field, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        field.onEdit(value, name, field, options);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -960,6 +948,71 @@ function addClearCallback(name, field, options) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initialize callbacks and initial states for groups
 | 
				
			||||||
 | 
					function initializeGroups(fields, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var modal = options.modal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Callback for when the group is expanded
 | 
				
			||||||
 | 
					    $(modal).find('.form-panel-content').on('show.bs.collapse', function() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var panel = $(this).closest('.form-panel');
 | 
				
			||||||
 | 
					        var group = panel.attr('group');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var icon = $(modal).find(`#group-icon-${group}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        icon.removeClass('fa-angle-right');
 | 
				
			||||||
 | 
					        icon.addClass('fa-angle-up');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Callback for when the group is collapsed
 | 
				
			||||||
 | 
					    $(modal).find('.form-panel-content').on('hide.bs.collapse', function() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var panel = $(this).closest('.form-panel');
 | 
				
			||||||
 | 
					        var group = panel.attr('group');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var icon = $(modal).find(`#group-icon-${group}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        icon.removeClass('fa-angle-up');
 | 
				
			||||||
 | 
					        icon.addClass('fa-angle-right');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Set initial state of each specified group
 | 
				
			||||||
 | 
					    for (var group in options.groups) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var group_options = options.groups[group];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (group_options.collapsed) {
 | 
				
			||||||
 | 
					            $(modal).find(`#form-panel-content-${group}`).collapse("hide");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $(modal).find(`#form-panel-content-${group}`).collapse("show");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (group_options.hidden) {
 | 
				
			||||||
 | 
					            hideFormGroup(group, options);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Hide a form group
 | 
				
			||||||
 | 
					function hideFormGroup(group, options) {
 | 
				
			||||||
 | 
					    $(options.modal).find(`#form-panel-${group}`).hide();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Show a form group
 | 
				
			||||||
 | 
					function showFormGroup(group, options) {
 | 
				
			||||||
 | 
					    $(options.modal).find(`#form-panel-${group}`).show();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setFormGroupVisibility(group, vis, options) {
 | 
				
			||||||
 | 
					    if (vis) {
 | 
				
			||||||
 | 
					        showFormGroup(group, options);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        hideFormGroup(group, options);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function initializeRelatedFields(fields, options) {
 | 
					function initializeRelatedFields(fields, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var field_names = options.field_names;
 | 
					    var field_names = options.field_names;
 | 
				
			||||||
@@ -1353,6 +1406,8 @@ function renderModelData(name, model, data, parameters, options) {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
function constructField(name, parameters, options) {
 | 
					function constructField(name, parameters, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var html = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Shortcut for simple visual fields
 | 
					    // Shortcut for simple visual fields
 | 
				
			||||||
    if (parameters.type == 'candy') {
 | 
					    if (parameters.type == 'candy') {
 | 
				
			||||||
        return constructCandyInput(name, parameters, options);
 | 
					        return constructCandyInput(name, parameters, options);
 | 
				
			||||||
@@ -1365,14 +1420,59 @@ function constructField(name, parameters, options) {
 | 
				
			|||||||
        return constructHiddenInput(name, parameters, options);
 | 
					        return constructHiddenInput(name, parameters, options);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Are we ending a group?
 | 
				
			||||||
 | 
					    if (options.current_group && parameters.group != options.current_group) {
 | 
				
			||||||
 | 
					        html += `</div></div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Null out the current "group" so we can start a new one
 | 
				
			||||||
 | 
					        options.current_group = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Are we starting a new group?
 | 
				
			||||||
 | 
					    if (parameters.group) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var group = parameters.group;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var group_options = options.groups[group] || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Are we starting a new group?
 | 
				
			||||||
 | 
					        // Add HTML for the start of a separate panel
 | 
				
			||||||
 | 
					        if (parameters.group != options.current_group) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            html += `
 | 
				
			||||||
 | 
					            <div class='panel form-panel' id='form-panel-${group}' group='${group}'>
 | 
				
			||||||
 | 
					                <div class='panel-heading form-panel-heading' id='form-panel-heading-${group}'>`;
 | 
				
			||||||
 | 
					            if (group_options.collapsible) {
 | 
				
			||||||
 | 
					                html += `
 | 
				
			||||||
 | 
					                <div data-toggle='collapse' data-target='#form-panel-content-${group}'>
 | 
				
			||||||
 | 
					                    <a href='#'><span id='group-icon-${group}' class='fas fa-angle-up'></span> 
 | 
				
			||||||
 | 
					                `;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                html += `<div>`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            html += `<h4 style='display: inline;'>${group_options.title || group}</h4>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (group_options.collapsible) {
 | 
				
			||||||
 | 
					                html += `</a>`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            html += `
 | 
				
			||||||
 | 
					                </div></div>
 | 
				
			||||||
 | 
					                <div class='panel-content form-panel-content' id='form-panel-content-${group}'>
 | 
				
			||||||
 | 
					            `;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Keep track of the group we are in
 | 
				
			||||||
 | 
					        options.current_group = group;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var form_classes = 'form-group';
 | 
					    var form_classes = 'form-group';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (parameters.errors) {
 | 
					    if (parameters.errors) {
 | 
				
			||||||
        form_classes += ' has-error';
 | 
					        form_classes += ' has-error';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    var html = '';
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Optional content to render before the field
 | 
					    // Optional content to render before the field
 | 
				
			||||||
    if (parameters.before) {
 | 
					    if (parameters.before) {
 | 
				
			||||||
        html += parameters.before;
 | 
					        html += parameters.before;
 | 
				
			||||||
@@ -1428,13 +1528,14 @@ function constructField(name, parameters, options) {
 | 
				
			|||||||
        html += `</div>`;   // input-group
 | 
					        html += `</div>`;   // input-group
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Div for error messages
 | 
					 | 
				
			||||||
    html += `<div id='errors-${name}'></div>`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (parameters.help_text) {
 | 
					    if (parameters.help_text) {
 | 
				
			||||||
        html += constructHelpText(name, parameters, options);
 | 
					        html += constructHelpText(name, parameters, options);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Div for error messages
 | 
				
			||||||
 | 
					    html += `<div id='errors-${name}'></div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    html += `</div>`;   // controls
 | 
					    html += `</div>`;   // controls
 | 
				
			||||||
    html += `</div>`;   // form-group
 | 
					    html += `</div>`;   // form-group
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -1599,6 +1700,10 @@ function constructInputOptions(name, classes, type, parameters) {
 | 
				
			|||||||
        opts.push(`placeholder='${parameters.placeholder}'`);
 | 
					        opts.push(`placeholder='${parameters.placeholder}'`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (parameters.type == 'boolean') {
 | 
				
			||||||
 | 
					        opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (parameters.multiline) {
 | 
					    if (parameters.multiline) {
 | 
				
			||||||
        return `<textarea ${opts.join(' ')}></textarea>`;
 | 
					        return `<textarea ${opts.join(' ')}></textarea>`;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@@ -1772,7 +1877,13 @@ function constructCandyInput(name, parameters, options) {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
function constructHelpText(name, parameters, options) {
 | 
					function constructHelpText(name, parameters, options) {
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    var html = `<div id='hint_id_${name}' class='help-block'><i>${parameters.help_text}</i></div>`;
 | 
					    var style = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (parameters.type == 'boolean') {
 | 
				
			||||||
 | 
					        style = `style='display: inline-block; margin-left: 25px' `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var html = `<div id='hint_id_${name}' ${style}class='help-block'><i>${parameters.help_text}</i></div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html;
 | 
					    return html;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -13,6 +13,31 @@ function yesNoLabel(value) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function partGroups(options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        attributes: {
 | 
				
			||||||
 | 
					            title: '{% trans "Part Attributes" %}',
 | 
				
			||||||
 | 
					            collapsible: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        create: {
 | 
				
			||||||
 | 
					            title: '{% trans "Part Creation Options" %}',
 | 
				
			||||||
 | 
					            collapsible: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        duplicate: {
 | 
				
			||||||
 | 
					            title: '{% trans "Part Duplication Options" %}',
 | 
				
			||||||
 | 
					            collapsible: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        supplier: {
 | 
				
			||||||
 | 
					            title: '{% trans "Supplier Options" %}',
 | 
				
			||||||
 | 
					            collapsible: true,
 | 
				
			||||||
 | 
					            hidden: !global_settings.PART_PURCHASEABLE,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Construct fieldset for part forms
 | 
					// Construct fieldset for part forms
 | 
				
			||||||
function partFields(options={}) {
 | 
					function partFields(options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,36 +73,44 @@ function partFields(options={}) {
 | 
				
			|||||||
        minimum_stock: {
 | 
					        minimum_stock: {
 | 
				
			||||||
            icon: 'fa-boxes',
 | 
					            icon: 'fa-boxes',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        attributes: {
 | 
					 | 
				
			||||||
            type: 'candy',
 | 
					 | 
				
			||||||
            html: `<hr><h4><i>{% trans "Part Attributes" %}</i></h4><hr>`
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        component: {
 | 
					        component: {
 | 
				
			||||||
            value: global_settings.PART_COMPONENT,
 | 
					            value: global_settings.PART_COMPONENT,
 | 
				
			||||||
 | 
					            group: 'attributes',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        assembly: {
 | 
					        assembly: {
 | 
				
			||||||
            value: global_settings.PART_ASSEMBLY,
 | 
					            value: global_settings.PART_ASSEMBLY,
 | 
				
			||||||
 | 
					            group: 'attributes',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        is_template: {
 | 
					        is_template: {
 | 
				
			||||||
            value: global_settings.PART_TEMPLATE,
 | 
					            value: global_settings.PART_TEMPLATE,
 | 
				
			||||||
 | 
					            group: 'attributes',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        trackable: {
 | 
					        trackable: {
 | 
				
			||||||
            value: global_settings.PART_TRACKABLE,
 | 
					            value: global_settings.PART_TRACKABLE,
 | 
				
			||||||
 | 
					            group: 'attributes',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        purchaseable: {
 | 
					        purchaseable: {
 | 
				
			||||||
            value: global_settings.PART_PURCHASEABLE,
 | 
					            value: global_settings.PART_PURCHASEABLE,
 | 
				
			||||||
 | 
					            group: 'attributes',
 | 
				
			||||||
 | 
					            onEdit: function(value, name, field, options) {
 | 
				
			||||||
 | 
					                setFormGroupVisibility('supplier', value, options);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        salable: {
 | 
					        salable: {
 | 
				
			||||||
            value: global_settings.PART_SALABLE,
 | 
					            value: global_settings.PART_SALABLE,
 | 
				
			||||||
 | 
					            group: 'attributes',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        virtual: {
 | 
					        virtual: {
 | 
				
			||||||
            value: global_settings.PART_VIRTUAL,
 | 
					            value: global_settings.PART_VIRTUAL,
 | 
				
			||||||
 | 
					            group: 'attributes',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If editing a part, we can set the "active" status
 | 
					    // If editing a part, we can set the "active" status
 | 
				
			||||||
    if (options.edit) {
 | 
					    if (options.edit) {
 | 
				
			||||||
        fields.active = {};
 | 
					        fields.active = {
 | 
				
			||||||
 | 
					            group: 'attributes'
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Pop expiry field
 | 
					    // Pop expiry field
 | 
				
			||||||
@@ -91,16 +124,32 @@ function partFields(options={}) {
 | 
				
			|||||||
        // No supplier parts available yet
 | 
					        // No supplier parts available yet
 | 
				
			||||||
        delete fields["default_supplier"];
 | 
					        delete fields["default_supplier"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fields.create = {
 | 
					        if (global_settings.PART_CREATE_INITIAL) {
 | 
				
			||||||
            type: 'candy',
 | 
					
 | 
				
			||||||
            html: `<hr><h4><i>{% trans "Part Creation Options" %}</i></h4><hr>`,
 | 
					            fields.initial_stock = {
 | 
				
			||||||
 | 
					                type: 'boolean',
 | 
				
			||||||
 | 
					                label: '{% trans "Create Initial Stock" %}',
 | 
				
			||||||
 | 
					                help_text: '{% trans "Create an initial stock item for this part" %}',
 | 
				
			||||||
 | 
					                group: 'create',
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (global_settings.PART_CREATE_INITIAL) {
 | 
					            fields.initial_stock_quantity = {
 | 
				
			||||||
            fields.initial_stock = {
 | 
					 | 
				
			||||||
                type: 'decimal',
 | 
					                type: 'decimal',
 | 
				
			||||||
 | 
					                value: 1,
 | 
				
			||||||
                label: '{% trans "Initial Stock Quantity" %}',
 | 
					                label: '{% trans "Initial Stock Quantity" %}',
 | 
				
			||||||
                help_text: '{% trans "Initialize part stock with specified quantity" %}',
 | 
					                help_text: '{% trans "Specify initial stock quantity for this part" %}',
 | 
				
			||||||
 | 
					                group: 'create',
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // TODO - Allow initial location of stock to be specified
 | 
				
			||||||
 | 
					            fields.initial_stock_location = {
 | 
				
			||||||
 | 
					                label: '{% trans "Location" %}',
 | 
				
			||||||
 | 
					                help_text: '{% trans "Select destination stock location" %}',
 | 
				
			||||||
 | 
					                type: 'related field',
 | 
				
			||||||
 | 
					                required: true,
 | 
				
			||||||
 | 
					                api_url: `/api/stock/location/`,
 | 
				
			||||||
 | 
					                model: 'stocklocation',
 | 
				
			||||||
 | 
					                group: 'create',
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -109,21 +158,65 @@ function partFields(options={}) {
 | 
				
			|||||||
            label: '{% trans "Copy Category Parameters" %}',
 | 
					            label: '{% trans "Copy Category Parameters" %}',
 | 
				
			||||||
            help_text: '{% trans "Copy parameter templates from selected part category" %}',
 | 
					            help_text: '{% trans "Copy parameter templates from selected part category" %}',
 | 
				
			||||||
            value: global_settings.PART_CATEGORY_PARAMETERS,
 | 
					            value: global_settings.PART_CATEGORY_PARAMETERS,
 | 
				
			||||||
 | 
					            group: 'create',
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Supplier options
 | 
				
			||||||
 | 
					        fields.add_supplier_info = {
 | 
				
			||||||
 | 
					            type: 'boolean',
 | 
				
			||||||
 | 
					            label: '{% trans "Add Supplier Data" %}',
 | 
				
			||||||
 | 
					            help_text: '{% trans "Create initial supplier data for this part" %}',
 | 
				
			||||||
 | 
					            group: 'supplier',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        fields.supplier = {
 | 
				
			||||||
 | 
					            type: 'related field',
 | 
				
			||||||
 | 
					            model: 'company',
 | 
				
			||||||
 | 
					            label: '{% trans "Supplier" %}',
 | 
				
			||||||
 | 
					            help_text: '{% trans "Select supplier" %}',
 | 
				
			||||||
 | 
					            filters: {
 | 
				
			||||||
 | 
					                'is_supplier': true,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            api_url: '{% url "api-company-list" %}',
 | 
				
			||||||
 | 
					            group: 'supplier',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        fields.SKU = {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            label: '{% trans "SKU" %}', 
 | 
				
			||||||
 | 
					            help_text: '{% trans "Supplier stock keeping unit" %}',
 | 
				
			||||||
 | 
					            group: 'supplier',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        fields.manufacturer = {
 | 
				
			||||||
 | 
					            type: 'related field',
 | 
				
			||||||
 | 
					            model: 'company',
 | 
				
			||||||
 | 
					            label: '{% trans "Manufacturer" %}',
 | 
				
			||||||
 | 
					            help_text: '{% trans "Select manufacturer" %}',
 | 
				
			||||||
 | 
					            filters: {
 | 
				
			||||||
 | 
					                'is_manufacturer': true,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            api_url: '{% url "api-company-list" %}',
 | 
				
			||||||
 | 
					            group: 'supplier',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        fields.MPN = {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            label: '{% trans "MPN" %}',
 | 
				
			||||||
 | 
					            help_text: '{% trans "Manufacturer Part Number" %}',
 | 
				
			||||||
 | 
					            group: 'supplier',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Additional fields when "duplicating" a part
 | 
					    // Additional fields when "duplicating" a part
 | 
				
			||||||
    if (options.duplicate) {
 | 
					    if (options.duplicate) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fields.duplicate = {
 | 
					 | 
				
			||||||
            type: 'candy',
 | 
					 | 
				
			||||||
            html: `<hr><h4><i>{% trans "Part Duplication Options" %}</i></h4><hr>`,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fields.copy_from = {
 | 
					        fields.copy_from = {
 | 
				
			||||||
            type: 'integer',
 | 
					            type: 'integer',
 | 
				
			||||||
            hidden: true,
 | 
					            hidden: true,
 | 
				
			||||||
            value: options.duplicate,
 | 
					            value: options.duplicate,
 | 
				
			||||||
 | 
					            group: 'duplicate',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fields.copy_image = {
 | 
					        fields.copy_image = {
 | 
				
			||||||
@@ -131,6 +224,7 @@ function partFields(options={}) {
 | 
				
			|||||||
            label: '{% trans "Copy Image" %}',
 | 
					            label: '{% trans "Copy Image" %}',
 | 
				
			||||||
            help_text: '{% trans "Copy image from original part" %}',
 | 
					            help_text: '{% trans "Copy image from original part" %}',
 | 
				
			||||||
            value: true,
 | 
					            value: true,
 | 
				
			||||||
 | 
					            group: 'duplicate',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fields.copy_bom = {
 | 
					        fields.copy_bom = {
 | 
				
			||||||
@@ -138,6 +232,7 @@ function partFields(options={}) {
 | 
				
			|||||||
            label: '{% trans "Copy BOM" %}',
 | 
					            label: '{% trans "Copy BOM" %}',
 | 
				
			||||||
            help_text: '{% trans "Copy bill of materials from original part" %}',
 | 
					            help_text: '{% trans "Copy bill of materials from original part" %}',
 | 
				
			||||||
            value: global_settings.PART_COPY_BOM,
 | 
					            value: global_settings.PART_COPY_BOM,
 | 
				
			||||||
 | 
					            group: 'duplicate',
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fields.copy_parameters = {
 | 
					        fields.copy_parameters = {
 | 
				
			||||||
@@ -145,6 +240,7 @@ function partFields(options={}) {
 | 
				
			|||||||
            label: '{% trans "Copy Parameters" %}',
 | 
					            label: '{% trans "Copy Parameters" %}',
 | 
				
			||||||
            help_text: '{% trans "Copy parameter data from original part" %}',
 | 
					            help_text: '{% trans "Copy parameter data from original part" %}',
 | 
				
			||||||
            value: global_settings.PART_COPY_PARAMETERS,
 | 
					            value: global_settings.PART_COPY_PARAMETERS,
 | 
				
			||||||
 | 
					            group: 'duplicate',
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -191,8 +287,11 @@ function editPart(pk, options={}) {
 | 
				
			|||||||
        edit: true
 | 
					        edit: true
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var groups = partGroups({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructForm(url, {
 | 
					    constructForm(url, {
 | 
				
			||||||
        fields: fields,
 | 
					        fields: fields,
 | 
				
			||||||
 | 
					        groups: partGroups(),
 | 
				
			||||||
        title: '{% trans "Edit Part" %}',
 | 
					        title: '{% trans "Edit Part" %}',
 | 
				
			||||||
        reload: true,
 | 
					        reload: true,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -221,6 +320,7 @@ function duplicatePart(pk, options={}) {
 | 
				
			|||||||
            constructForm('{% url "api-part-list" %}', {
 | 
					            constructForm('{% url "api-part-list" %}', {
 | 
				
			||||||
                method: 'POST',
 | 
					                method: 'POST',
 | 
				
			||||||
                fields: fields,
 | 
					                fields: fields,
 | 
				
			||||||
 | 
					                groups: partGroups(),
 | 
				
			||||||
                title: '{% trans "Duplicate Part" %}',
 | 
					                title: '{% trans "Duplicate Part" %}',
 | 
				
			||||||
                data: data,
 | 
					                data: data,
 | 
				
			||||||
                onSuccess: function(data) {
 | 
					                onSuccess: function(data) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user