mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge branch 'search-autocomplete' of https://github.com/matmair/InvenTree into search-autocomplete
This commit is contained in:
		@@ -8,12 +8,16 @@ import re
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import common.models
 | 
					import common.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INVENTREE_SW_VERSION = "0.3.0"
 | 
					INVENTREE_SW_VERSION = "0.3.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INVENTREE_API_VERSION = 7
 | 
					INVENTREE_API_VERSION = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Increment thi API version number whenever there is a significant change to the API that any clients need to know about
 | 
					Increment this API version number whenever there is a significant change to the API that any clients need to know about
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					v8 -> 2021-07-19
 | 
				
			||||||
 | 
					    - Refactors the API interface for SupplierPart and ManufacturerPart models
 | 
				
			||||||
 | 
					    - ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
v7 -> 2021-07-03
 | 
					v7 -> 2021-07-03
 | 
				
			||||||
    - Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716
 | 
					    - Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -233,6 +233,13 @@ class InvenTreeSetting(models.Model):
 | 
				
			|||||||
            'validator': bool,
 | 
					            'validator': bool,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        'PART_CREATE_INITIAL': {
 | 
				
			||||||
 | 
					            'name': _('Create initial stock'),
 | 
				
			||||||
 | 
					            'description': _('Create initial stock on part creation'),
 | 
				
			||||||
 | 
					            'default': False,
 | 
				
			||||||
 | 
					            'validator': bool,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        'PART_INTERNAL_PRICE': {
 | 
					        'PART_INTERNAL_PRICE': {
 | 
				
			||||||
            'name': _('Internal Prices'),
 | 
					            'name': _('Internal Prices'),
 | 
				
			||||||
            'description': _('Enable internal prices for parts'),
 | 
					            'description': _('Enable internal prices for parts'),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,13 +6,12 @@ Django Forms for interacting with Company app
 | 
				
			|||||||
from __future__ import unicode_literals
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from InvenTree.forms import HelperForm
 | 
					from InvenTree.forms import HelperForm
 | 
				
			||||||
from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
 | 
					from InvenTree.fields import RoundingDecimalFormField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.utils.translation import ugettext_lazy as _
 | 
					from django.utils.translation import ugettext_lazy as _
 | 
				
			||||||
import django.forms
 | 
					import django.forms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Company
 | 
					from .models import Company
 | 
				
			||||||
from .models import SupplierPart
 | 
					 | 
				
			||||||
from .models import SupplierPriceBreak
 | 
					from .models import SupplierPriceBreak
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,67 +33,6 @@ class CompanyImageDownloadForm(HelperForm):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EditSupplierPartForm(HelperForm):
 | 
					 | 
				
			||||||
    """ Form for editing a SupplierPart object """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    field_prefix = {
 | 
					 | 
				
			||||||
        'link': 'fa-link',
 | 
					 | 
				
			||||||
        'SKU': 'fa-hashtag',
 | 
					 | 
				
			||||||
        'note': 'fa-pencil-alt',
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    single_pricing = InvenTreeMoneyField(
 | 
					 | 
				
			||||||
        label=_('Single Price'),
 | 
					 | 
				
			||||||
        help_text=_('Single quantity price'),
 | 
					 | 
				
			||||||
        decimal_places=4,
 | 
					 | 
				
			||||||
        max_digits=19,
 | 
					 | 
				
			||||||
        required=False,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    manufacturer = django.forms.ChoiceField(
 | 
					 | 
				
			||||||
        required=False,
 | 
					 | 
				
			||||||
        help_text=_('Select manufacturer'),
 | 
					 | 
				
			||||||
        choices=[],
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    MPN = django.forms.CharField(
 | 
					 | 
				
			||||||
        required=False,
 | 
					 | 
				
			||||||
        help_text=_('Manufacturer Part Number'),
 | 
					 | 
				
			||||||
        max_length=100,
 | 
					 | 
				
			||||||
        label=_('MPN'),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = SupplierPart
 | 
					 | 
				
			||||||
        fields = [
 | 
					 | 
				
			||||||
            'part',
 | 
					 | 
				
			||||||
            'supplier',
 | 
					 | 
				
			||||||
            'SKU',
 | 
					 | 
				
			||||||
            'manufacturer',
 | 
					 | 
				
			||||||
            'MPN',
 | 
					 | 
				
			||||||
            'description',
 | 
					 | 
				
			||||||
            'link',
 | 
					 | 
				
			||||||
            'note',
 | 
					 | 
				
			||||||
            'single_pricing',
 | 
					 | 
				
			||||||
            # 'base_cost',
 | 
					 | 
				
			||||||
            # 'multiple',
 | 
					 | 
				
			||||||
            'packaging',
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_manufacturer_choices(self):
 | 
					 | 
				
			||||||
        """ Returns tuples for all manufacturers """
 | 
					 | 
				
			||||||
        empty_choice = [('', '----------')]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return empty_choice + manufacturers
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.fields['manufacturer'].choices = self.get_manufacturer_choices()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class EditPriceBreakForm(HelperForm):
 | 
					class EditPriceBreakForm(HelperForm):
 | 
				
			||||||
    """ Form for creating / editing a supplier price break """
 | 
					    """ Form for creating / editing a supplier price break """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,9 +9,7 @@ import os
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.utils.translation import ugettext_lazy as _
 | 
					from django.utils.translation import ugettext_lazy as _
 | 
				
			||||||
from django.core.validators import MinValueValidator
 | 
					from django.core.validators import MinValueValidator
 | 
				
			||||||
from django.core.exceptions import ValidationError
 | 
					 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.db.utils import IntegrityError
 | 
					 | 
				
			||||||
from django.db.models import Sum, Q, UniqueConstraint
 | 
					from django.db.models import Sum, Q, UniqueConstraint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import apps
 | 
					from django.apps import apps
 | 
				
			||||||
@@ -475,57 +473,6 @@ class SupplierPart(models.Model):
 | 
				
			|||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        return reverse('supplier-part-detail', kwargs={'pk': self.id})
 | 
					        return reverse('supplier-part-detail', kwargs={'pk': self.id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        """ Overriding save method to process the linked ManufacturerPart
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if 'manufacturer' in kwargs:
 | 
					 | 
				
			||||||
            manufacturer_id = kwargs.pop('manufacturer')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                manufacturer = Company.objects.get(pk=int(manufacturer_id))
 | 
					 | 
				
			||||||
            except (ValueError, Company.DoesNotExist):
 | 
					 | 
				
			||||||
                manufacturer = None
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            manufacturer = None
 | 
					 | 
				
			||||||
        if 'MPN' in kwargs:
 | 
					 | 
				
			||||||
            MPN = kwargs.pop('MPN')
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            MPN = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if manufacturer or MPN:
 | 
					 | 
				
			||||||
            if not self.manufacturer_part:
 | 
					 | 
				
			||||||
                # Create ManufacturerPart
 | 
					 | 
				
			||||||
                manufacturer_part = ManufacturerPart.create(part=self.part,
 | 
					 | 
				
			||||||
                                                            manufacturer=manufacturer,
 | 
					 | 
				
			||||||
                                                            mpn=MPN,
 | 
					 | 
				
			||||||
                                                            description=self.description)
 | 
					 | 
				
			||||||
                self.manufacturer_part = manufacturer_part
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                # Update ManufacturerPart (if ID exists)
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    manufacturer_part_id = self.manufacturer_part.id
 | 
					 | 
				
			||||||
                except AttributeError:
 | 
					 | 
				
			||||||
                    manufacturer_part_id = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if manufacturer_part_id:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        (manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part,
 | 
					 | 
				
			||||||
                                                                                                 manufacturer=manufacturer,
 | 
					 | 
				
			||||||
                                                                                                 MPN=MPN)
 | 
					 | 
				
			||||||
                    except IntegrityError:
 | 
					 | 
				
			||||||
                        manufacturer_part = None
 | 
					 | 
				
			||||||
                        raise ValidationError(f'ManufacturerPart linked to {self.part} from manufacturer {manufacturer.name}'
 | 
					 | 
				
			||||||
                                              f'with part number {MPN} already exists!')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if manufacturer_part:
 | 
					 | 
				
			||||||
                    self.manufacturer_part = manufacturer_part
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.clean()
 | 
					 | 
				
			||||||
        self.validate_unique()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        super().save(*args, **kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        unique_together = ('part', 'supplier', 'SKU')
 | 
					        unique_together = ('part', 'supplier', 'SKU')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,7 +96,9 @@ class CompanySerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ManufacturerPartSerializer(InvenTreeModelSerializer):
 | 
					class ManufacturerPartSerializer(InvenTreeModelSerializer):
 | 
				
			||||||
    """ Serializer for ManufacturerPart object """
 | 
					    """
 | 
				
			||||||
 | 
					    Serializer for ManufacturerPart object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
 | 
					    part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -106,8 +108,8 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        part_detail = kwargs.pop('part_detail', False)
 | 
					        part_detail = kwargs.pop('part_detail', True)
 | 
				
			||||||
        manufacturer_detail = kwargs.pop('manufacturer_detail', False)
 | 
					        manufacturer_detail = kwargs.pop('manufacturer_detail', True)
 | 
				
			||||||
        prettify = kwargs.pop('pretty', False)
 | 
					        prettify = kwargs.pop('pretty', False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        super(ManufacturerPartSerializer, self).__init__(*args, **kwargs)
 | 
					        super(ManufacturerPartSerializer, self).__init__(*args, **kwargs)
 | 
				
			||||||
@@ -229,25 +231,6 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
            'supplier_detail',
 | 
					            'supplier_detail',
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data):
 | 
					 | 
				
			||||||
        """ Extract manufacturer data and process ManufacturerPart """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Create SupplierPart
 | 
					 | 
				
			||||||
        supplier_part = super().create(validated_data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Get ManufacturerPart raw data (unvalidated)
 | 
					 | 
				
			||||||
        manufacturer_id = self.initial_data.get('manufacturer', None)
 | 
					 | 
				
			||||||
        MPN = self.initial_data.get('MPN', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if manufacturer_id and MPN:
 | 
					 | 
				
			||||||
            kwargs = {
 | 
					 | 
				
			||||||
                'manufacturer': manufacturer_id,
 | 
					 | 
				
			||||||
                'MPN': MPN,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            supplier_part.save(**kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return supplier_part
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
 | 
					class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
 | 
				
			||||||
    """ Serializer for SupplierPriceBreak object """
 | 
					    """ Serializer for SupplierPriceBreak object """
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -267,16 +267,8 @@
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#stock-export").click(function() {
 | 
					    $("#stock-export").click(function() {
 | 
				
			||||||
        launchModalForm("{% url 'stock-export-options' %}", {
 | 
					        exportStock({
 | 
				
			||||||
            submit_text: '{% trans "Export" %}',
 | 
					            supplier: {{ company.id }}
 | 
				
			||||||
            success: function(response) {
 | 
					 | 
				
			||||||
                var url = "{% url 'stock-export' %}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                url += "?format=" + response.format;
 | 
					 | 
				
			||||||
                url += "&supplier={{ company.id }}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                location.href = url;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -284,22 +276,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    $("#manufacturer-part-create").click(function () {
 | 
					    $("#manufacturer-part-create").click(function () {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        constructForm('{% url "api-manufacturer-part-list" %}', {
 | 
					        createManufacturerPart({
 | 
				
			||||||
            fields: {
 | 
					            manufacturer: {{ company.pk }},
 | 
				
			||||||
                part: {},
 | 
					 | 
				
			||||||
                manufacturer: {
 | 
					 | 
				
			||||||
                    value: {{ company.pk }},
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                MPN: {
 | 
					 | 
				
			||||||
                    icon: 'fa-hashtag',
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                description: {},
 | 
					 | 
				
			||||||
                link: {
 | 
					 | 
				
			||||||
                    icon: 'fa-link',
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            method: 'POST',
 | 
					 | 
				
			||||||
            title: '{% trans "Add Manufacturer Part" %}',
 | 
					 | 
				
			||||||
            onSuccess: function() {
 | 
					            onSuccess: function() {
 | 
				
			||||||
                $("#part-table").bootstrapTable("refresh");
 | 
					                $("#part-table").bootstrapTable("refresh");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -350,27 +328,15 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    {% if company.is_supplier %}
 | 
					    {% if company.is_supplier %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function reloadSupplierPartTable() {
 | 
				
			||||||
 | 
					        $('#supplier-part-table').bootstrapTable('refresh');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#supplier-part-create").click(function () {
 | 
					    $("#supplier-part-create").click(function () {
 | 
				
			||||||
        launchModalForm(
 | 
					
 | 
				
			||||||
            "{% url 'supplier-part-create' %}",
 | 
					        createSupplierPart({
 | 
				
			||||||
            {
 | 
					            supplier: {{ company.pk }},
 | 
				
			||||||
                data: {
 | 
					            onSuccess: reloadSupplierPartTable,
 | 
				
			||||||
                    supplier: {{ company.id }},
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                reload: true,
 | 
					 | 
				
			||||||
                secondary: [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        field: 'part',
 | 
					 | 
				
			||||||
                        label: '{% trans "New Part" %}',
 | 
					 | 
				
			||||||
                        title: '{% trans "Create new Part" %}',
 | 
					 | 
				
			||||||
                        url: "{% url 'part-create' %}"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        field: 'supplier',
 | 
					 | 
				
			||||||
                        label: "{% trans 'New Supplier' %}",
 | 
					 | 
				
			||||||
                        title: "{% trans 'Create new Supplier' %}",
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -390,22 +356,27 @@
 | 
				
			|||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#multi-part-delete").click(function() {
 | 
					    $("#multi-part-delete").click(function() {
 | 
				
			||||||
        var selections = $("#part-table").bootstrapTable("getSelections");
 | 
					        var selections = $("#supplier-part-table").bootstrapTable("getSelections");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var parts = [];
 | 
					        var requests = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        selections.forEach(function(item) {
 | 
					        showQuestionDialog(
 | 
				
			||||||
            parts.push(item.pk);
 | 
					            '{% trans "Delete Supplier Parts?" %}',
 | 
				
			||||||
 | 
					            '{% trans "All selected supplier parts will be deleted" %}',
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                accept: function() {
 | 
				
			||||||
 | 
					                    selections.forEach(function(part) {
 | 
				
			||||||
 | 
					                        var url = `/api/company/part/${part.pk}/`;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					                        requests.push(inventreeDelete(url));
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        var url = "{% url 'supplier-part-delete' %}"
 | 
					                    $.when.apply($, requests).then(function() {
 | 
				
			||||||
 | 
					                        $('#supplier-part-table').bootstrapTable('refresh');
 | 
				
			||||||
        launchModalForm(url, {
 | 
					 | 
				
			||||||
            data: {
 | 
					 | 
				
			||||||
                parts: parts,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            reload: true,
 | 
					 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#multi-part-order").click(function() {
 | 
					    $("#multi-part-order").click(function() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -178,21 +178,15 @@ $('#parameter-create').click(function() {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function reloadSupplierPartTable() {
 | 
				
			||||||
 | 
					    $('#supplier-table').bootstrapTable('refresh');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$('#supplier-create').click(function () {
 | 
					$('#supplier-create').click(function () {
 | 
				
			||||||
    launchModalForm(
 | 
					    createSupplierPart({
 | 
				
			||||||
        "{% url 'supplier-part-create' %}",
 | 
					        manufacturer_part: {{ part.pk }},
 | 
				
			||||||
        {
 | 
					        part: {{ part.part.pk }},
 | 
				
			||||||
            reload: true,
 | 
					        onSuccess: reloadSupplierPartTable,
 | 
				
			||||||
            data: {
 | 
					 | 
				
			||||||
                manufacturer_part: {{ part.id }}
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            secondary: [
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    field: 'supplier',
 | 
					 | 
				
			||||||
                    label: '{% trans "New Supplier" %}',
 | 
					 | 
				
			||||||
                    title: '{% trans "Create new supplier" %}',
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -200,18 +194,25 @@ $("#supplier-part-delete").click(function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    var selections = $("#supplier-table").bootstrapTable("getSelections");
 | 
					    var selections = $("#supplier-table").bootstrapTable("getSelections");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var parts = [];
 | 
					    var requests = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    selections.forEach(function(item) {
 | 
					    showQuestionDialog(
 | 
				
			||||||
        parts.push(item.pk);
 | 
					        '{% trans "Delete Supplier Parts?" %}',
 | 
				
			||||||
 | 
					        '{% trans "All selected supplier parts will be deleted" %}',
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            accept: function() {
 | 
				
			||||||
 | 
					                selections.forEach(function(part) {
 | 
				
			||||||
 | 
					                    var url = `/api/company/part/${part.pk}/`;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					                    requests.push(inventreeDelete(url));
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
    launchModalForm("{% url 'supplier-part-delete' %}", {
 | 
					                $.when.apply($, requests).then(function() {
 | 
				
			||||||
        data: {
 | 
					                    reloadSupplierPartTable();
 | 
				
			||||||
            parts: parts,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        reload: true,
 | 
					 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#multi-parameter-delete").click(function() {
 | 
					$("#multi-parameter-delete").click(function() {
 | 
				
			||||||
@@ -296,29 +297,19 @@ $('#order-part, #order-part2').click(function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
$('#edit-part').click(function () {
 | 
					$('#edit-part').click(function () {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructForm('{% url "api-manufacturer-part-detail" part.pk %}', {
 | 
					    editManufacturerPart({{ part.pk }}, {
 | 
				
			||||||
        fields: {
 | 
					        onSuccess: function() {
 | 
				
			||||||
            part: {},
 | 
					            location.reload();
 | 
				
			||||||
            manufacturer: {},
 | 
					        }
 | 
				
			||||||
            MPN: {
 | 
					 | 
				
			||||||
                icon: 'fa-hashtag',
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            description: {},
 | 
					 | 
				
			||||||
            link: {
 | 
					 | 
				
			||||||
                icon: 'fa-link',
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        title: '{% trans "Edit Manufacturer Part" %}',
 | 
					 | 
				
			||||||
        reload: true,
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$('#delete-part').click(function() {
 | 
					$('#delete-part').click(function() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructForm('{% url "api-manufacturer-part-detail" part.pk %}', {
 | 
					    deleteManufacturerPart({{ part.pk }}, {
 | 
				
			||||||
        method: 'DELETE',
 | 
					        onSuccess: function() {
 | 
				
			||||||
        title: '{% trans "Delete Manufacturer Part" %}',
 | 
					            window.location.href = "{% url 'company-detail' part.manufacturer.id %}";
 | 
				
			||||||
        redirect: "{% url 'company-detail' part.manufacturer.id %}",
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@
 | 
				
			|||||||
    </li>
 | 
					    </li>
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {% if company.is_supplier or company.is_manufacturer %}
 | 
					    {% if company.is_supplier %}
 | 
				
			||||||
    <li class='list-group-item' title='{% trans "Supplied Parts" %}'>
 | 
					    <li class='list-group-item' title='{% trans "Supplied Parts" %}'>
 | 
				
			||||||
        <a href='#' id='select-supplier-parts' class='nav-toggle'>
 | 
					        <a href='#' id='select-supplier-parts' class='nav-toggle'>
 | 
				
			||||||
            <span class='fas fa-building sidebar-icon'></span>
 | 
					            <span class='fas fa-building sidebar-icon'></span>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -284,18 +284,11 @@ loadStockTable($("#stock-table"), {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#stock-export").click(function() {
 | 
					$("#stock-export").click(function() {
 | 
				
			||||||
    launchModalForm("{% url 'stock-export-options' %}", {
 | 
					 | 
				
			||||||
        submit_text: '{% trans "Export" %}',
 | 
					 | 
				
			||||||
        success: function(response) {
 | 
					 | 
				
			||||||
            var url = "{% url 'stock-export' %}";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            url += "?format=" + response.format;
 | 
					    exportStock({
 | 
				
			||||||
            url += "&cascade=" + response.cascade;
 | 
					        supplier_part: {{ part.pk }},
 | 
				
			||||||
            url += "&supplier_part={{ part.id }}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            location.href = url;
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#item-create").click(function() {
 | 
					$("#item-create").click(function() {
 | 
				
			||||||
@@ -327,21 +320,21 @@ $('#order-part, #order-part2').click(function() {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$('#edit-part').click(function () {
 | 
					$('#edit-part').click(function () {
 | 
				
			||||||
    launchModalForm(
 | 
					
 | 
				
			||||||
                    "{% url 'supplier-part-edit' part.id %}",
 | 
					    editSupplierPart({{ part.pk }}, {
 | 
				
			||||||
                    {
 | 
					        onSuccess: function() {
 | 
				
			||||||
                        reload: true
 | 
					            location.reload();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    );
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$('#delete-part').click(function() {
 | 
					$('#delete-part').click(function() {
 | 
				
			||||||
    launchModalForm(
 | 
					
 | 
				
			||||||
        "{% url 'supplier-part-delete' %}?part={{ part.id }}",
 | 
					    deleteSupplierPart({{ part.pk }}, {
 | 
				
			||||||
        {
 | 
					        onSuccess: function() {
 | 
				
			||||||
            redirect: "{% url 'company-detail' part.supplier.id %}"
 | 
					            window.location.href = "{% url 'company-detail' part.supplier.id %}";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    );
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
attachNavCallbacks({
 | 
					attachNavCallbacks({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +0,0 @@
 | 
				
			|||||||
{% extends "modal_form.html" %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% load i18n %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% block pre_form_content %}
 | 
					 | 
				
			||||||
{{ block.super }}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% if part %}
 | 
					 | 
				
			||||||
<div class='alert alert-block alert-info'>
 | 
					 | 
				
			||||||
    {% include "hover_image.html" with image=part.image %}
 | 
					 | 
				
			||||||
    {{ part.full_name}}
 | 
					 | 
				
			||||||
    <br>
 | 
					 | 
				
			||||||
    <i>{{ part.description }}</i>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
{% endif %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					 | 
				
			||||||
@@ -1,31 +0,0 @@
 | 
				
			|||||||
{% extends "modal_delete_form.html" %}
 | 
					 | 
				
			||||||
{% load i18n %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% block pre_form_content %}
 | 
					 | 
				
			||||||
{% trans "Are you sure you want to delete the following Supplier Parts?" %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<hr>
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% block form_data %}
 | 
					 | 
				
			||||||
<table class='table table-striped table-condensed'>
 | 
					 | 
				
			||||||
{% for part in parts %}
 | 
					 | 
				
			||||||
<tr>
 | 
					 | 
				
			||||||
    <input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <td>
 | 
					 | 
				
			||||||
        {% include "hover_image.html" with image=part.part.image %}
 | 
					 | 
				
			||||||
        {{ part.part.full_name }}
 | 
					 | 
				
			||||||
    </td>
 | 
					 | 
				
			||||||
    <td>
 | 
					 | 
				
			||||||
        {% include "hover_image.html" with image=part.supplier.image %}
 | 
					 | 
				
			||||||
        {{ part.supplier.name }}
 | 
					 | 
				
			||||||
    </td>
 | 
					 | 
				
			||||||
    <td>
 | 
					 | 
				
			||||||
        {{ part.SKU }}
 | 
					 | 
				
			||||||
    </td>
 | 
					 | 
				
			||||||
</tr>
 | 
					 | 
				
			||||||
{% endfor %}
 | 
					 | 
				
			||||||
</table>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					 | 
				
			||||||
@@ -218,14 +218,27 @@ class ManufacturerTest(InvenTreeAPITestCase):
 | 
				
			|||||||
    def test_supplier_part_create(self):
 | 
					    def test_supplier_part_create(self):
 | 
				
			||||||
        url = reverse('api-supplier-part-list')
 | 
					        url = reverse('api-supplier-part-list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create supplier part
 | 
					        # Create a manufacturer part
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            reverse('api-manufacturer-part-list'),
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'part': 1,
 | 
				
			||||||
 | 
					                'manufacturer': 7,
 | 
				
			||||||
 | 
					                'MPN': 'PART_NUMBER',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=201
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pk = response.data['pk']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Create a supplier part (associated with the new manufacturer part)
 | 
				
			||||||
        data = {
 | 
					        data = {
 | 
				
			||||||
            'part': 1,
 | 
					            'part': 1,
 | 
				
			||||||
            'supplier': 1,
 | 
					            'supplier': 1,
 | 
				
			||||||
            'SKU': 'SKU_TEST',
 | 
					            'SKU': 'SKU_TEST',
 | 
				
			||||||
            'manufacturer': 7,
 | 
					            'manufacturer_part': pk,
 | 
				
			||||||
            'MPN': 'PART_NUMBER',
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(url, data, format='json')
 | 
					        response = self.client.post(url, data, format='json')
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 | 
					        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,9 +10,6 @@ from django.urls import reverse
 | 
				
			|||||||
from django.contrib.auth import get_user_model
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
from django.contrib.auth.models import Group
 | 
					from django.contrib.auth.models import Group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import ManufacturerPart
 | 
					 | 
				
			||||||
from .models import SupplierPart
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CompanyViewTestBase(TestCase):
 | 
					class CompanyViewTestBase(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,108 +72,6 @@ class CompanyViewTestBase(TestCase):
 | 
				
			|||||||
        return json_data, form_errors
 | 
					        return json_data, form_errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SupplierPartViewTests(CompanyViewTestBase):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Tests for the SupplierPart views.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_supplier_part_create(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Test the SupplierPartCreate view.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        This view allows some additional functionality,
 | 
					 | 
				
			||||||
        specifically it allows the user to create a single-quantity price break
 | 
					 | 
				
			||||||
        automatically, when saving the new SupplierPart model.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        url = reverse('supplier-part-create')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # First check that we can GET the form
 | 
					 | 
				
			||||||
        response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # How many supplier parts are already in the database?
 | 
					 | 
				
			||||||
        n = SupplierPart.objects.all().count()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            'part': 1,
 | 
					 | 
				
			||||||
            'supplier': 1,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # SKU is required! (form should fail)
 | 
					 | 
				
			||||||
        (response, errors) = self.post(url, data, valid=False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertIsNotNone(errors.get('SKU', None))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        data['SKU'] = 'TEST-ME-123'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        (response, errors) = self.post(url, data, valid=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check that the SupplierPart was created!
 | 
					 | 
				
			||||||
        self.assertEqual(n + 1, SupplierPart.objects.all().count())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check that it was created *without* a price-break
 | 
					 | 
				
			||||||
        supplier_part = SupplierPart.objects.get(pk=response['pk'])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(supplier_part.price_breaks.count(), 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Duplicate SKU is prohibited
 | 
					 | 
				
			||||||
        (response, errors) = self.post(url, data, valid=False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertIsNotNone(errors.get('__all__', None))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Add with a different SKU, *and* a single-quantity price
 | 
					 | 
				
			||||||
        data['SKU'] = 'TEST-ME-1234'
 | 
					 | 
				
			||||||
        data['single_pricing_0'] = '123.4'
 | 
					 | 
				
			||||||
        data['single_pricing_1'] = 'CAD'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        (response, errors) = self.post(url, data, valid=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        pk = response.get('pk')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check that *another* SupplierPart was created
 | 
					 | 
				
			||||||
        self.assertEqual(n + 2, SupplierPart.objects.all().count())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        supplier_part = SupplierPart.objects.get(pk=pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check that a price-break has been created!
 | 
					 | 
				
			||||||
        self.assertEqual(supplier_part.price_breaks.count(), 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        price_break = supplier_part.price_breaks.first()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(price_break.quantity, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_supplier_part_delete(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Test the SupplierPartDelete view
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        url = reverse('supplier-part-delete')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Get form using 'part' argument
 | 
					 | 
				
			||||||
        response = self.client.get(url, {'part': '1'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Get form using 'parts' argument
 | 
					 | 
				
			||||||
        response = self.client.get(url + '?parts[]=1&parts[]=2', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # POST to delete two parts
 | 
					 | 
				
			||||||
        n = SupplierPart.objects.count()
 | 
					 | 
				
			||||||
        response = self.client.post(
 | 
					 | 
				
			||||||
            url,
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                'supplier-part-2': 'supplier-part-2',
 | 
					 | 
				
			||||||
                'supplier-part-3': 'supplier-part-3',
 | 
					 | 
				
			||||||
                'confirm_delete': True
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            HTTP_X_REQUESTED_WITH='XMLHttpRequest')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(n - 2, SupplierPart.objects.count())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CompanyViewTest(CompanyViewTestBase):
 | 
					class CompanyViewTest(CompanyViewTestBase):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Tests for various 'Company' views
 | 
					    Tests for various 'Company' views
 | 
				
			||||||
@@ -187,36 +82,3 @@ class CompanyViewTest(CompanyViewTestBase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(reverse('company-index'))
 | 
					        response = self.client.get(reverse('company-index'))
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ManufacturerPartViewTests(CompanyViewTestBase):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Tests for the ManufacturerPart views.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_supplier_part_create(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Test that the SupplierPartCreate view creates Manufacturer Part.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        url = reverse('supplier-part-create')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # First check that we can GET the form
 | 
					 | 
				
			||||||
        response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # How many manufacturer parts are already in the database?
 | 
					 | 
				
			||||||
        n = ManufacturerPart.objects.all().count()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            'part': 1,
 | 
					 | 
				
			||||||
            'supplier': 1,
 | 
					 | 
				
			||||||
            'SKU': 'SKU_TEST',
 | 
					 | 
				
			||||||
            'manufacturer': 6,
 | 
					 | 
				
			||||||
            'MPN': 'MPN_TEST',
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        (response, errors) = self.post(url, data, valid=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check that the ManufacturerPart was created!
 | 
					 | 
				
			||||||
        self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -192,18 +192,14 @@ class ManufacturerPartSimpleTest(TestCase):
 | 
				
			|||||||
            SKU='SKU_TEST',
 | 
					            SKU='SKU_TEST',
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        kwargs = {
 | 
					        supplier_part.save()
 | 
				
			||||||
            'manufacturer': manufacturer.id,
 | 
					 | 
				
			||||||
            'MPN': 'MPN_TEST',
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        supplier_part.save(**kwargs)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_exists(self):
 | 
					    def test_exists(self):
 | 
				
			||||||
        self.assertEqual(ManufacturerPart.objects.count(), 5)
 | 
					        self.assertEqual(ManufacturerPart.objects.count(), 4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Check that manufacturer part was created from supplier part creation
 | 
					        # Check that manufacturer part was created from supplier part creation
 | 
				
			||||||
        manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=1)
 | 
					        manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=1)
 | 
				
			||||||
        self.assertEqual(manufacturer_parts.count(), 2)
 | 
					        self.assertEqual(manufacturer_parts.count(), 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_delete(self):
 | 
					    def test_delete(self):
 | 
				
			||||||
        # Remove a part
 | 
					        # Remove a part
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,16 +35,6 @@ manufacturer_part_urls = [
 | 
				
			|||||||
    ])),
 | 
					    ])),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
supplier_part_detail_urls = [
 | 
					supplier_part_urls = [
 | 
				
			||||||
    url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
 | 
					    url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					 | 
				
			||||||
supplier_part_urls = [
 | 
					 | 
				
			||||||
    url(r'^new/?', views.SupplierPartCreate.as_view(), name='supplier-part-create'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    url(r'delete/', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,31 +10,22 @@ from django.utils.translation import ugettext_lazy as _
 | 
				
			|||||||
from django.views.generic import DetailView, ListView
 | 
					from django.views.generic import DetailView, ListView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.forms import HiddenInput
 | 
					 | 
				
			||||||
from django.core.files.base import ContentFile
 | 
					from django.core.files.base import ContentFile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from moneyed import CURRENCIES
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from PIL import Image
 | 
					from PIL import Image
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
import io
 | 
					import io
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
 | 
					from InvenTree.views import AjaxUpdateView
 | 
				
			||||||
from InvenTree.helpers import str2bool
 | 
					 | 
				
			||||||
from InvenTree.views import InvenTreeRoleMixin
 | 
					from InvenTree.views import InvenTreeRoleMixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Company
 | 
					from .models import Company
 | 
				
			||||||
from .models import ManufacturerPart
 | 
					from .models import ManufacturerPart
 | 
				
			||||||
from .models import SupplierPart
 | 
					from .models import SupplierPart
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from part.models import Part
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .forms import EditSupplierPartForm
 | 
					 | 
				
			||||||
from .forms import CompanyImageDownloadForm
 | 
					from .forms import CompanyImageDownloadForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import common.models
 | 
					 | 
				
			||||||
import common.settings
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CompanyIndex(InvenTreeRoleMixin, ListView):
 | 
					class CompanyIndex(InvenTreeRoleMixin, ListView):
 | 
				
			||||||
    """ View for displaying list of companies
 | 
					    """ View for displaying list of companies
 | 
				
			||||||
@@ -231,272 +222,3 @@ class SupplierPartDetail(DetailView):
 | 
				
			|||||||
        ctx = super().get_context_data(**kwargs)
 | 
					        ctx = super().get_context_data(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return ctx
 | 
					        return ctx
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SupplierPartEdit(AjaxUpdateView):
 | 
					 | 
				
			||||||
    """ Update view for editing SupplierPart """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    model = SupplierPart
 | 
					 | 
				
			||||||
    context_object_name = 'part'
 | 
					 | 
				
			||||||
    form_class = EditSupplierPartForm
 | 
					 | 
				
			||||||
    ajax_template_name = 'modal_form.html'
 | 
					 | 
				
			||||||
    ajax_form_title = _('Edit Supplier Part')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def save(self, supplier_part, form, **kwargs):
 | 
					 | 
				
			||||||
        """ Process ManufacturerPart data """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manufacturer = form.cleaned_data.get('manufacturer', None)
 | 
					 | 
				
			||||||
        MPN = form.cleaned_data.get('MPN', None)
 | 
					 | 
				
			||||||
        kwargs = {'manufacturer': manufacturer,
 | 
					 | 
				
			||||||
                  'MPN': MPN,
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
        supplier_part.save(**kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_form(self):
 | 
					 | 
				
			||||||
        form = super().get_form()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        supplier_part = self.get_object()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Hide Manufacturer fields
 | 
					 | 
				
			||||||
        form.fields['manufacturer'].widget = HiddenInput()
 | 
					 | 
				
			||||||
        form.fields['MPN'].widget = HiddenInput()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # It appears that hiding a MoneyField fails validation
 | 
					 | 
				
			||||||
        # Therefore the idea to set the value before hiding
 | 
					 | 
				
			||||||
        if form.is_valid():
 | 
					 | 
				
			||||||
            form.cleaned_data['single_pricing'] = supplier_part.unit_pricing
 | 
					 | 
				
			||||||
        # Hide the single-pricing field (only for creating a new SupplierPart!)
 | 
					 | 
				
			||||||
        form.fields['single_pricing'].widget = HiddenInput()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return form
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_initial(self):
 | 
					 | 
				
			||||||
        """ Fetch data from ManufacturerPart """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        initials = super(SupplierPartEdit, self).get_initial().copy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        supplier_part = self.get_object()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if supplier_part.manufacturer_part:
 | 
					 | 
				
			||||||
            if supplier_part.manufacturer_part.manufacturer:
 | 
					 | 
				
			||||||
                initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
 | 
					 | 
				
			||||||
            initials['MPN'] = supplier_part.manufacturer_part.MPN
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return initials
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SupplierPartCreate(AjaxCreateView):
 | 
					 | 
				
			||||||
    """ Create view for making new SupplierPart """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    model = SupplierPart
 | 
					 | 
				
			||||||
    form_class = EditSupplierPartForm
 | 
					 | 
				
			||||||
    ajax_template_name = 'company/supplier_part_create.html'
 | 
					 | 
				
			||||||
    ajax_form_title = _('Create new Supplier Part')
 | 
					 | 
				
			||||||
    context_object_name = 'part'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def validate(self, part, form):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        single_pricing = form.cleaned_data.get('single_pricing', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if single_pricing:
 | 
					 | 
				
			||||||
            # TODO - What validation steps can be performed on the single_pricing field?
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_context_data(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Supply context data to the form
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ctx = super().get_context_data()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Add 'part' object
 | 
					 | 
				
			||||||
        form = self.get_form()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        part = form['part'].value()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            part = Part.objects.get(pk=part)
 | 
					 | 
				
			||||||
        except (ValueError, Part.DoesNotExist):
 | 
					 | 
				
			||||||
            part = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ctx['part'] = part
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return ctx
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def save(self, form):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        If single_pricing is defined, add a price break for quantity=1
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Save the supplier part object
 | 
					 | 
				
			||||||
        supplier_part = super().save(form)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Process manufacturer data
 | 
					 | 
				
			||||||
        manufacturer = form.cleaned_data.get('manufacturer', None)
 | 
					 | 
				
			||||||
        MPN = form.cleaned_data.get('MPN', None)
 | 
					 | 
				
			||||||
        kwargs = {'manufacturer': manufacturer,
 | 
					 | 
				
			||||||
                  'MPN': MPN,
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
        supplier_part.save(**kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        single_pricing = form.cleaned_data.get('single_pricing', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if single_pricing:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            supplier_part.add_price_break(1, single_pricing)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return supplier_part
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_form(self):
 | 
					 | 
				
			||||||
        """ Create Form instance to create a new SupplierPart object.
 | 
					 | 
				
			||||||
        Hide some fields if they are not appropriate in context
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        form = super(AjaxCreateView, self).get_form()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if form.initial.get('part', None):
 | 
					 | 
				
			||||||
            # Hide the part field
 | 
					 | 
				
			||||||
            form.fields['part'].widget = HiddenInput()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if form.initial.get('manufacturer', None):
 | 
					 | 
				
			||||||
            # Hide the manufacturer field
 | 
					 | 
				
			||||||
            form.fields['manufacturer'].widget = HiddenInput()
 | 
					 | 
				
			||||||
            # Hide the MPN field
 | 
					 | 
				
			||||||
            form.fields['MPN'].widget = HiddenInput()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return form
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_initial(self):
 | 
					 | 
				
			||||||
        """ Provide initial data for new SupplierPart:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        - If 'supplier_id' provided, pre-fill supplier field
 | 
					 | 
				
			||||||
        - If 'part_id' provided, pre-fill part field
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        initials = super(SupplierPartCreate, self).get_initial().copy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manufacturer_id = self.get_param('manufacturer')
 | 
					 | 
				
			||||||
        supplier_id = self.get_param('supplier')
 | 
					 | 
				
			||||||
        part_id = self.get_param('part')
 | 
					 | 
				
			||||||
        manufacturer_part_id = self.get_param('manufacturer_part')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        supplier = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if supplier_id:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                supplier = Company.objects.get(pk=supplier_id)
 | 
					 | 
				
			||||||
                initials['supplier'] = supplier
 | 
					 | 
				
			||||||
            except (ValueError, Company.DoesNotExist):
 | 
					 | 
				
			||||||
                supplier = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if manufacturer_id:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
 | 
					 | 
				
			||||||
            except (ValueError, Company.DoesNotExist):
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if manufacturer_part_id:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                # Get ManufacturerPart instance information
 | 
					 | 
				
			||||||
                manufacturer_part_obj = ManufacturerPart.objects.get(pk=manufacturer_part_id)
 | 
					 | 
				
			||||||
                initials['part'] = Part.objects.get(pk=manufacturer_part_obj.part.id)
 | 
					 | 
				
			||||||
                initials['manufacturer'] = manufacturer_part_obj.manufacturer.id
 | 
					 | 
				
			||||||
                initials['MPN'] = manufacturer_part_obj.MPN
 | 
					 | 
				
			||||||
            except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if part_id:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                initials['part'] = Part.objects.get(pk=part_id)
 | 
					 | 
				
			||||||
            except (ValueError, Part.DoesNotExist):
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Initial value for single pricing
 | 
					 | 
				
			||||||
        if supplier:
 | 
					 | 
				
			||||||
            currency_code = supplier.currency_code
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            currency_code = common.settings.currency_code_default()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        currency = CURRENCIES.get(currency_code, None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if currency_code:
 | 
					 | 
				
			||||||
            initials['single_pricing'] = ('', currency)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return initials
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SupplierPartDelete(AjaxDeleteView):
 | 
					 | 
				
			||||||
    """ Delete view for removing a SupplierPart.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    SupplierParts can be deleted using a variety of 'selectors'.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    - ?part=<pk> -> Delete a single SupplierPart object
 | 
					 | 
				
			||||||
    - ?parts=[] -> Delete a list of SupplierPart objects
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    success_url = '/supplier/'
 | 
					 | 
				
			||||||
    ajax_template_name = 'company/supplier_part_delete.html'
 | 
					 | 
				
			||||||
    ajax_form_title = _('Delete Supplier Part')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    role_required = 'purchase_order.delete'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parts = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_context_data(self):
 | 
					 | 
				
			||||||
        ctx = {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ctx['parts'] = self.parts
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return ctx
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_parts(self):
 | 
					 | 
				
			||||||
        """ Determine which SupplierPart object(s) the user wishes to delete.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.parts = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # User passes a single SupplierPart ID
 | 
					 | 
				
			||||||
        if 'part' in self.request.GET:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                self.parts.append(SupplierPart.objects.get(pk=self.request.GET.get('part')))
 | 
					 | 
				
			||||||
            except (ValueError, SupplierPart.DoesNotExist):
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif 'parts[]' in self.request.GET:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            part_id_list = self.request.GET.getlist('parts[]')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            self.parts = SupplierPart.objects.filter(id__in=part_id_list)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        self.request = request
 | 
					 | 
				
			||||||
        self.get_parts()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.renderJsonResponse(request, form=self.get_form())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        """ Handle the POST action for deleting supplier parts.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.request = request
 | 
					 | 
				
			||||||
        self.parts = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for item in self.request.POST:
 | 
					 | 
				
			||||||
            if item.startswith('supplier-part-'):
 | 
					 | 
				
			||||||
                pk = item.replace('supplier-part-', '')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    self.parts.append(SupplierPart.objects.get(pk=pk))
 | 
					 | 
				
			||||||
                except (ValueError, SupplierPart.DoesNotExist):
 | 
					 | 
				
			||||||
                    pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        confirm = str2bool(self.request.POST.get('confirm_delete', False))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            'form_valid': confirm,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if confirm:
 | 
					 | 
				
			||||||
            for part in self.parts:
 | 
					 | 
				
			||||||
                part.delete()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.renderJsonResponse(self.request, data=data, form=self.get_form())
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -132,7 +132,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
 | 
					    purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    destination = LocationBriefSerializer(source='get_destination', read_only=True)
 | 
					    destination_detail = LocationBriefSerializer(source='get_destination', read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    purchase_price_currency = serializers.ChoiceField(
 | 
					    purchase_price_currency = serializers.ChoiceField(
 | 
				
			||||||
        choices=currency_code_mappings(),
 | 
					        choices=currency_code_mappings(),
 | 
				
			||||||
@@ -156,6 +156,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
            'purchase_price_currency',
 | 
					            'purchase_price_currency',
 | 
				
			||||||
            'purchase_price_string',
 | 
					            'purchase_price_string',
 | 
				
			||||||
            'destination',
 | 
					            'destination',
 | 
				
			||||||
 | 
					            'destination_detail',
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -170,6 +170,7 @@ $("#edit-order").click(function() {
 | 
				
			|||||||
            supplier: {
 | 
					            supplier: {
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					            supplier_reference: {},
 | 
				
			||||||
            description: {},
 | 
					            description: {},
 | 
				
			||||||
            target_date: {
 | 
					            target_date: {
 | 
				
			||||||
                icon: 'fa-calendar-alt',
 | 
					                icon: 'fa-calendar-alt',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -401,8 +401,15 @@ $("#po-table").inventreeTable({
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            field: 'destination.pathstring',
 | 
					            field: 'destination',
 | 
				
			||||||
            title: '{% trans "Destination" %}',
 | 
					            title: '{% trans "Destination" %}',
 | 
				
			||||||
 | 
					            formatter: function(value, row) {
 | 
				
			||||||
 | 
					                if (value) {
 | 
				
			||||||
 | 
					                    return renderLink(row.destination_detail.pathstring, `/stock/location/${value}/`);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    return '-';
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            field: 'notes',
 | 
					            field: 'notes',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -163,6 +163,7 @@ $("#edit-order").click(function() {
 | 
				
			|||||||
            customer: {
 | 
					            customer: {
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					            customer_reference: {},
 | 
				
			||||||
            description: {},
 | 
					            description: {},
 | 
				
			||||||
            target_date: {
 | 
					            target_date: {
 | 
				
			||||||
                icon: 'fa-calendar-alt',
 | 
					                icon: 'fa-calendar-alt',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,9 +118,17 @@ class CategoryList(generics.ListCreateAPIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    ordering_fields = [
 | 
					    ordering_fields = [
 | 
				
			||||||
        'name',
 | 
					        'name',
 | 
				
			||||||
 | 
					        'level',
 | 
				
			||||||
 | 
					        'tree_id',
 | 
				
			||||||
 | 
					        'lft',
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ordering = 'name'
 | 
					    # Use hierarchical ordering by default
 | 
				
			||||||
 | 
					    ordering = [
 | 
				
			||||||
 | 
					        'tree_id',
 | 
				
			||||||
 | 
					        'lft',
 | 
				
			||||||
 | 
					        'name'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    search_fields = [
 | 
					    search_fields = [
 | 
				
			||||||
        'name',
 | 
					        'name',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -217,6 +217,11 @@ class EditPartForm(HelperForm):
 | 
				
			|||||||
                                                   label=_('Include parent categories parameter templates'),
 | 
					                                                   label=_('Include parent categories parameter templates'),
 | 
				
			||||||
                                                   widget=forms.HiddenInput())
 | 
					                                                   widget=forms.HiddenInput())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial_stock = forms.IntegerField(required=False,
 | 
				
			||||||
 | 
					                                       initial=0,
 | 
				
			||||||
 | 
					                                       label=_('Initial stock amount'),
 | 
				
			||||||
 | 
					                                       help_text=_('Create stock for this part'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Part
 | 
					        model = Part
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
@@ -238,6 +243,7 @@ class EditPartForm(HelperForm):
 | 
				
			|||||||
            'default_expiry',
 | 
					            'default_expiry',
 | 
				
			||||||
            'units',
 | 
					            'units',
 | 
				
			||||||
            'minimum_stock',
 | 
					            'minimum_stock',
 | 
				
			||||||
 | 
					            'initial_stock',
 | 
				
			||||||
            'component',
 | 
					            'component',
 | 
				
			||||||
            'assembly',
 | 
					            'assembly',
 | 
				
			||||||
            'is_template',
 | 
					            'is_template',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,8 @@ class CategorySerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    parts = serializers.IntegerField(source='item_count', read_only=True)
 | 
					    parts = serializers.IntegerField(source='item_count', read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    level = serializers.IntegerField(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = PartCategory
 | 
					        model = PartCategory
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
@@ -40,10 +42,11 @@ class CategorySerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
            'description',
 | 
					            'description',
 | 
				
			||||||
            'default_location',
 | 
					            'default_location',
 | 
				
			||||||
            'default_keywords',
 | 
					            'default_keywords',
 | 
				
			||||||
            'pathstring',
 | 
					            'level',
 | 
				
			||||||
            'url',
 | 
					 | 
				
			||||||
            'parent',
 | 
					            'parent',
 | 
				
			||||||
            'parts',
 | 
					            'parts',
 | 
				
			||||||
 | 
					            'pathstring',
 | 
				
			||||||
 | 
					            'url',
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -370,6 +370,16 @@
 | 
				
			|||||||
        sub_part_detail: true,
 | 
					        sub_part_detail: true,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Load the BOM table data in the pricing view
 | 
				
			||||||
 | 
					    loadBomTable($("#bom-pricing-table"), {
 | 
				
			||||||
 | 
					        editable: {{ editing_enabled }},
 | 
				
			||||||
 | 
					        bom_url: "{% url 'api-bom-list' %}",
 | 
				
			||||||
 | 
					        part_url: "{% url 'api-part-list' %}",
 | 
				
			||||||
 | 
					        parent_id: {{ part.id }} ,
 | 
				
			||||||
 | 
					        sub_part_detail: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    linkButtonsToSelection($("#bom-table"),
 | 
					    linkButtonsToSelection($("#bom-table"),
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
            "#bom-item-delete",
 | 
					            "#bom-item-delete",
 | 
				
			||||||
@@ -634,17 +644,9 @@
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#stock-export").click(function() {
 | 
					    $("#stock-export").click(function() {
 | 
				
			||||||
        launchModalForm("{% url 'stock-export-options' %}", {
 | 
					 | 
				
			||||||
            submit_text: "{% trans 'Export' %}",
 | 
					 | 
				
			||||||
            success: function(response) {
 | 
					 | 
				
			||||||
                var url = "{% url 'stock-export' %}";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                url += "?format=" + response.format;
 | 
					        exportStock({
 | 
				
			||||||
                url += "&cascade=" + response.cascade;
 | 
					            part: {{ part.pk }}
 | 
				
			||||||
                url += "&part={{ part.id }}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                location.href = url;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -801,26 +803,15 @@
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('#supplier-create').click(function () {
 | 
					    function reloadSupplierPartTable() {
 | 
				
			||||||
        launchModalForm(
 | 
					        $('#supplier-part-table').bootstrapTable('refresh');
 | 
				
			||||||
            "{% url 'supplier-part-create' %}",
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                reload: true,
 | 
					 | 
				
			||||||
                data: {
 | 
					 | 
				
			||||||
                    part: {{ part.id }}
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                secondary: [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        field: 'supplier',
 | 
					 | 
				
			||||||
                        label: '{% trans "New Supplier" %}',
 | 
					 | 
				
			||||||
                        title: '{% trans "Create new supplier" %}',
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        field: 'manufacturer',
 | 
					 | 
				
			||||||
                        label: '{% trans "New Manufacturer" %}',
 | 
					 | 
				
			||||||
                        title: '{% trans "Create new manufacturer" %}',
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
                ]
 | 
					
 | 
				
			||||||
 | 
					    $('#supplier-create').click(function () {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        createSupplierPart({
 | 
				
			||||||
 | 
					            part: {{ part.pk }},
 | 
				
			||||||
 | 
					            onSuccess: reloadSupplierPartTable,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -828,18 +819,25 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        var selections = $("#supplier-part-table").bootstrapTable("getSelections");
 | 
					        var selections = $("#supplier-part-table").bootstrapTable("getSelections");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var parts = [];
 | 
					        var requests = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        selections.forEach(function(item) {
 | 
					        showQuestionDialog(
 | 
				
			||||||
            parts.push(item.pk);
 | 
					            '{% trans "Delete Supplier Parts?" %}',
 | 
				
			||||||
 | 
					            '{% trans "All selected supplier parts will be deleted" %}',
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                accept: function() {
 | 
				
			||||||
 | 
					                    selections.forEach(function(part) {
 | 
				
			||||||
 | 
					                        var url = `/api/company/part/${part.pk}/`;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					                        requests.push(inventreeDelete(url));
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        launchModalForm("{% url 'supplier-part-delete' %}", {
 | 
					                    $.when.apply($, requests).then(function() {
 | 
				
			||||||
            data: {
 | 
					                        reloadSupplierPartTable();
 | 
				
			||||||
                parts: parts,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            reload: true,
 | 
					 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    loadSupplierPartTable(
 | 
					    loadSupplierPartTable(
 | 
				
			||||||
@@ -884,19 +882,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    $('#manufacturer-create').click(function () {
 | 
					    $('#manufacturer-create').click(function () {
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        constructForm('{% url "api-manufacturer-part-list" %}', {
 | 
					        createManufacturerPart({
 | 
				
			||||||
            fields: {
 | 
					            part: {{ part.pk }},
 | 
				
			||||||
                part: {
 | 
					 | 
				
			||||||
                    value: {{ part.pk }},
 | 
					 | 
				
			||||||
                    hidden: true,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                manufacturer: {},
 | 
					 | 
				
			||||||
                MPN: {},
 | 
					 | 
				
			||||||
                description: {},
 | 
					 | 
				
			||||||
                link: {},
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            method: 'POST',
 | 
					 | 
				
			||||||
            title: '{% trans "Add Manufacturer Part" %}',
 | 
					 | 
				
			||||||
            onSuccess: function() {
 | 
					            onSuccess: function() {
 | 
				
			||||||
                $("#manufacturer-part-table").bootstrapTable("refresh");
 | 
					                $("#manufacturer-part-table").bootstrapTable("refresh");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -217,7 +217,7 @@
 | 
				
			|||||||
<div class='panel-content'>
 | 
					<div class='panel-content'>
 | 
				
			||||||
    <div class="row">
 | 
					    <div class="row">
 | 
				
			||||||
        <div class="col col-md-6">
 | 
					        <div class="col col-md-6">
 | 
				
			||||||
            <table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-table'></table>
 | 
					            <table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-pricing-table'></table>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {% if part.bom_count > 0 %}
 | 
					        {% if part.bom_count > 0 %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,7 +44,7 @@ from common.files import FileManager
 | 
				
			|||||||
from common.views import FileManagementFormView, FileManagementAjaxView
 | 
					from common.views import FileManagementFormView, FileManagementAjaxView
 | 
				
			||||||
from common.forms import UploadFileForm, MatchFieldForm
 | 
					from common.forms import UploadFileForm, MatchFieldForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from stock.models import StockLocation
 | 
					from stock.models import StockItem, StockLocation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import common.settings as inventree_settings
 | 
					import common.settings as inventree_settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -487,6 +487,10 @@ class PartCreate(AjaxCreateView):
 | 
				
			|||||||
        if not inventree_settings.stock_expiry_enabled():
 | 
					        if not inventree_settings.stock_expiry_enabled():
 | 
				
			||||||
            form.fields['default_expiry'].widget = HiddenInput()
 | 
					            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!)
 | 
					        # Hide the default_supplier field (there are no matching supplier parts yet!)
 | 
				
			||||||
        form.fields['default_supplier'].widget = HiddenInput()
 | 
					        form.fields['default_supplier'].widget = HiddenInput()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -547,6 +551,14 @@ class PartCreate(AjaxCreateView):
 | 
				
			|||||||
            # Save part and pass category template settings
 | 
					            # Save part and pass category template settings
 | 
				
			||||||
            part.save(**{'add_category_templates': add_category_templates})
 | 
					            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['pk'] = part.pk
 | 
				
			||||||
            data['text'] = str(part)
 | 
					            data['text'] = str(part)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -357,7 +357,8 @@ class TestReport(ReportTemplateBase):
 | 
				
			|||||||
            'serial': stock_item.serial,
 | 
					            'serial': stock_item.serial,
 | 
				
			||||||
            'part': stock_item.part,
 | 
					            'part': stock_item.part,
 | 
				
			||||||
            'results': stock_item.testResultMap(include_installed=self.include_installed),
 | 
					            'results': stock_item.testResultMap(include_installed=self.include_installed),
 | 
				
			||||||
            'result_list': stock_item.testResultList(include_installed=self.include_installed)
 | 
					            'result_list': stock_item.testResultList(include_installed=self.include_installed),
 | 
				
			||||||
 | 
					            'installed_items': stock_item.get_installed_items(cascade=True),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -56,6 +56,10 @@ content: "{% trans 'Stock Item Test Report' %}";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block pre_page_content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block page_content %}
 | 
					{% block page_content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class='container'>
 | 
					<div class='container'>
 | 
				
			||||||
@@ -80,6 +84,7 @@ content: "{% trans 'Stock Item Test Report' %}";
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% if resul_list|length > 0 %}
 | 
				
			||||||
<h3>{% trans "Test Results" %}</h3>
 | 
					<h3>{% trans "Test Results" %}</h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<table class='table test-table'>
 | 
					<table class='table test-table'>
 | 
				
			||||||
@@ -112,5 +117,37 @@ content: "{% trans 'Stock Item Test Report' %}";
 | 
				
			|||||||
    </tbody>
 | 
					    </tbody>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</table>
 | 
					</table>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% if installed_items|length > 0 %}
 | 
				
			||||||
 | 
					<h3>{% trans "Installed Items" %}</h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<table class='table test-table'>
 | 
				
			||||||
 | 
					    <thead>
 | 
				
			||||||
 | 
					    </thead>
 | 
				
			||||||
 | 
					    <tbody>
 | 
				
			||||||
 | 
					    {% for sub_item in installed_items %}
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td>
 | 
				
			||||||
 | 
					                <img src='{% part_image sub_item.part %}' class='part-img' style='max-width: 24px; max-height: 24px;'>
 | 
				
			||||||
 | 
					                {{ sub_item.part.full_name }}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td>
 | 
				
			||||||
 | 
					                {% if sub_item.serialized %}
 | 
				
			||||||
 | 
					                {% trans "Serial" %}: {{ sub_item.serial }}
 | 
				
			||||||
 | 
					                {% else %}
 | 
				
			||||||
 | 
					                {% trans "Quantity" %}: {% decimal sub_item.quantity %}
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					    </tbody>
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block post_page_content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
@@ -363,6 +363,15 @@ class StockLocationList(generics.ListCreateAPIView):
 | 
				
			|||||||
    ordering_fields = [
 | 
					    ordering_fields = [
 | 
				
			||||||
        'name',
 | 
					        'name',
 | 
				
			||||||
        'items',
 | 
					        'items',
 | 
				
			||||||
 | 
					        'level',
 | 
				
			||||||
 | 
					        'tree_id',
 | 
				
			||||||
 | 
					        'lft',
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ordering = [
 | 
				
			||||||
 | 
					        'tree_id',
 | 
				
			||||||
 | 
					        'lft',
 | 
				
			||||||
 | 
					        'name',
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,6 @@ from django.core.exceptions import ValidationError
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from mptt.fields import TreeNodeChoiceField
 | 
					from mptt.fields import TreeNodeChoiceField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from InvenTree.helpers import GetExportFormats
 | 
					 | 
				
			||||||
from InvenTree.forms import HelperForm
 | 
					from InvenTree.forms import HelperForm
 | 
				
			||||||
from InvenTree.fields import RoundingDecimalFormField
 | 
					from InvenTree.fields import RoundingDecimalFormField
 | 
				
			||||||
from InvenTree.fields import DatePickerFormField
 | 
					from InvenTree.fields import DatePickerFormField
 | 
				
			||||||
@@ -226,33 +225,6 @@ class TestReportFormatForm(HelperForm):
 | 
				
			|||||||
    template = forms.ChoiceField(label=_('Template'), help_text=_('Select test report template'))
 | 
					    template = forms.ChoiceField(label=_('Template'), help_text=_('Select test report template'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExportOptionsForm(HelperForm):
 | 
					 | 
				
			||||||
    """ Form for selecting stock export options """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    file_format = forms.ChoiceField(label=_('File Format'), help_text=_('Select output file format'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    include_sublocations = forms.BooleanField(required=False, initial=True, label=_('Include sublocations'), help_text=_("Include stock items in sub locations"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = StockLocation
 | 
					 | 
				
			||||||
        fields = [
 | 
					 | 
				
			||||||
            'file_format',
 | 
					 | 
				
			||||||
            'include_sublocations',
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_format_choices(self):
 | 
					 | 
				
			||||||
        """ File format choices """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        choices = [(x, x.upper()) for x in GetExportFormats()]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return choices
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.fields['file_format'].choices = self.get_format_choices()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class InstallStockForm(HelperForm):
 | 
					class InstallStockForm(HelperForm):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Form for manually installing a stock item into another stock item
 | 
					    Form for manually installing a stock item into another stock item
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -260,12 +260,15 @@ class LocationSerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    items = serializers.IntegerField(source='item_count', read_only=True)
 | 
					    items = serializers.IntegerField(source='item_count', read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    level = serializers.IntegerField(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = StockLocation
 | 
					        model = StockLocation
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            'pk',
 | 
					            'pk',
 | 
				
			||||||
            'url',
 | 
					            'url',
 | 
				
			||||||
            'name',
 | 
					            'name',
 | 
				
			||||||
 | 
					            'level',
 | 
				
			||||||
            'description',
 | 
					            'description',
 | 
				
			||||||
            'parent',
 | 
					            'parent',
 | 
				
			||||||
            'pathstring',
 | 
					            'pathstring',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -227,20 +227,11 @@
 | 
				
			|||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#stock-export").click(function() {
 | 
					    $("#stock-export").click(function() {
 | 
				
			||||||
        launchModalForm("{% url 'stock-export-options' %}", {
 | 
					 | 
				
			||||||
            submit_text: '{% trans "Export" %}',
 | 
					 | 
				
			||||||
            success: function(response) {
 | 
					 | 
				
			||||||
                var url = "{% url 'stock-export' %}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                url += "?format=" + response.format;
 | 
					 | 
				
			||||||
                url += "&cascade=" + response.cascade;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        exportStock({
 | 
				
			||||||
            {% if location %}
 | 
					            {% if location %}
 | 
				
			||||||
                url += "&location={{ location.id }}";
 | 
					            location: {{ location.pk }}
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					 | 
				
			||||||
                location.href = url;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -56,7 +56,6 @@ stock_urls = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    url(r'^track/', include(stock_tracking_urls)),
 | 
					    url(r'^track/', include(stock_tracking_urls)),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    url(r'^export-options/?', views.StockExportOptions.as_view(), name='stock-export-options'),
 | 
					 | 
				
			||||||
    url(r'^export/?', views.StockExport.as_view(), name='stock-export'),
 | 
					    url(r'^export/?', views.StockExport.as_view(), name='stock-export'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Individual stock items
 | 
					    # Individual stock items
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -378,38 +378,6 @@ class StockItemDeleteTestData(AjaxUpdateView):
 | 
				
			|||||||
        return self.renderJsonResponse(request, form, data)
 | 
					        return self.renderJsonResponse(request, form, data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockExportOptions(AjaxView):
 | 
					 | 
				
			||||||
    """ Form for selecting StockExport options """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    model = StockLocation
 | 
					 | 
				
			||||||
    ajax_form_title = _('Stock Export Options')
 | 
					 | 
				
			||||||
    form_class = StockForms.ExportOptionsForm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.request = request
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fmt = request.POST.get('file_format', 'csv').lower()
 | 
					 | 
				
			||||||
        cascade = str2bool(request.POST.get('include_sublocations', False))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Format a URL to redirect to
 | 
					 | 
				
			||||||
        url = reverse('stock-export')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        url += '?format=' + fmt
 | 
					 | 
				
			||||||
        url += '&cascade=' + str(cascade)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            'form_valid': True,
 | 
					 | 
				
			||||||
            'format': fmt,
 | 
					 | 
				
			||||||
            'cascade': cascade
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.renderJsonResponse(self.request, self.form_class(), data=data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        return self.renderJsonResponse(request, self.form_class())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class StockExport(AjaxView):
 | 
					class StockExport(AjaxView):
 | 
				
			||||||
    """ Export stock data from a particular location.
 | 
					    """ Export stock data from a particular location.
 | 
				
			||||||
    Returns a file containing stock information for that location.
 | 
					    Returns a file containing stock information for that location.
 | 
				
			||||||
@@ -471,11 +439,10 @@ class StockExport(AjaxView):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if location:
 | 
					        if location:
 | 
				
			||||||
            # CHeck if locations should be cascading
 | 
					            # Check if locations should be cascading
 | 
				
			||||||
            cascade = str2bool(request.GET.get('cascade', True))
 | 
					            cascade = str2bool(request.GET.get('cascade', True))
 | 
				
			||||||
            stock_items = location.get_stock_items(cascade)
 | 
					            stock_items = location.get_stock_items(cascade)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            cascade = True
 | 
					 | 
				
			||||||
            stock_items = StockItem.objects.all()
 | 
					            stock_items = StockItem.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if part:
 | 
					        if part:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,7 @@
 | 
				
			|||||||
        {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %}
 | 
				
			||||||
 | 
					        {% include "InvenTree/settings/setting.html" with key="PART_CREATE_INITIAL" icon="fa-boxes" %}
 | 
				
			||||||
        <tr><td colspan='5'></td></tr>
 | 
					        <tr><td colspan='5'></td></tr>
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
 | 
				
			||||||
        {% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %}
 | 
					        {% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,142 @@
 | 
				
			|||||||
{% load i18n %}
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function manufacturerPartFields() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        part: {},
 | 
				
			||||||
 | 
					        manufacturer: {},
 | 
				
			||||||
 | 
					        MPN: {
 | 
				
			||||||
 | 
					            icon: 'fa-hashtag',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        description: {},
 | 
				
			||||||
 | 
					        link: {
 | 
				
			||||||
 | 
					            icon: 'fa-link',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createManufacturerPart(options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var fields = manufacturerPartFields();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options.part) {
 | 
				
			||||||
 | 
					        fields.part.value = options.part;
 | 
				
			||||||
 | 
					        fields.part.hidden = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options.manufacturer) {
 | 
				
			||||||
 | 
					        fields.manufacturer.value = options.manufacturer;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructForm('{% url "api-manufacturer-part-list" %}', {
 | 
				
			||||||
 | 
					        fields: fields,
 | 
				
			||||||
 | 
					        method: 'POST',
 | 
				
			||||||
 | 
					        title: '{% trans "Add Manufacturer Part" %}',
 | 
				
			||||||
 | 
					        onSuccess: options.onSuccess
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function editManufacturerPart(part, options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var url = `/api/company/part/manufacturer/${part}/`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructForm(url, {
 | 
				
			||||||
 | 
					        fields: manufacturerPartFields(),
 | 
				
			||||||
 | 
					        title: '{% trans "Edit Manufacturer Part" %}',
 | 
				
			||||||
 | 
					        onSuccess: options.onSuccess
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deleteManufacturerPart(part, options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructForm(`/api/company/part/manufacturer/${part}/`, {
 | 
				
			||||||
 | 
					        method: 'DELETE',
 | 
				
			||||||
 | 
					        title: '{% trans "Delete Manufacturer Part" %}',
 | 
				
			||||||
 | 
					        onSuccess: options.onSuccess,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function supplierPartFields() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        part: {},
 | 
				
			||||||
 | 
					        supplier: {},
 | 
				
			||||||
 | 
					        SKU: {
 | 
				
			||||||
 | 
					            icon: 'fa-hashtag',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        manufacturer_part: {
 | 
				
			||||||
 | 
					            filters: {
 | 
				
			||||||
 | 
					                part_detail: true,
 | 
				
			||||||
 | 
					                manufacturer_detail: true,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        description: {},
 | 
				
			||||||
 | 
					        link: {
 | 
				
			||||||
 | 
					            icon: 'fa-link',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        note: {
 | 
				
			||||||
 | 
					            icon: 'fa-pencil-alt',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        packaging: {
 | 
				
			||||||
 | 
					            icon: 'fa-box',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Launch a form to create a new ManufacturerPart
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function createSupplierPart(options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var fields = supplierPartFields();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options.part) {
 | 
				
			||||||
 | 
					        fields.manufacturer_part.filters.part = options.part;
 | 
				
			||||||
 | 
					        fields.part.hidden = true;
 | 
				
			||||||
 | 
					        fields.part.value = options.part;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options.supplier) {
 | 
				
			||||||
 | 
					        fields.supplier.value = options.supplier;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options.manufacturer_part) {
 | 
				
			||||||
 | 
					        fields.manufacturer_part.value = options.manufacturer_part;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructForm('{% url "api-supplier-part-list" %}', {
 | 
				
			||||||
 | 
					        fields: fields,
 | 
				
			||||||
 | 
					        method: 'POST',
 | 
				
			||||||
 | 
					        title: '{% trans "Add Supplier Part" %}',
 | 
				
			||||||
 | 
					        onSuccess: options.onSuccess,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function editSupplierPart(part, options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructForm(`/api/company/part/${part}/`, {
 | 
				
			||||||
 | 
					        fields: supplierPartFields(),
 | 
				
			||||||
 | 
					        title: '{% trans "Edit Supplier Part" %}',
 | 
				
			||||||
 | 
					        onSuccess: options.onSuccess
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deleteSupplierPart(part, options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructForm(`/api/company/part/${part}/`, {
 | 
				
			||||||
 | 
					        method: 'DELETE',
 | 
				
			||||||
 | 
					        title: '{% trans "Delete Supplier Part" %}',
 | 
				
			||||||
 | 
					        onSuccess: options.onSuccess,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Returns a default form-set for creating / editing a Company object
 | 
					// Returns a default form-set for creating / editing a Company object
 | 
				
			||||||
function companyFormFields(options={}) {
 | 
					function companyFormFields(options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -323,8 +459,52 @@ function loadManufacturerPartTable(table, url, options) {
 | 
				
			|||||||
                title: '{% trans "Description" %}',
 | 
					                title: '{% trans "Description" %}',
 | 
				
			||||||
                sortable: false,
 | 
					                sortable: false,
 | 
				
			||||||
                switchable: true,
 | 
					                switchable: true,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                field: 'actions',
 | 
				
			||||||
 | 
					                title: '',
 | 
				
			||||||
 | 
					                sortable: false,
 | 
				
			||||||
 | 
					                switchable: false,
 | 
				
			||||||
 | 
					                formatter: function(value, row) {
 | 
				
			||||||
 | 
					                    var pk = row.pk;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    var html = `<div class='btn-group float-right' role='group'>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    html += makeIconButton('fa-edit icon-blue', 'button-manufacturer-part-edit', pk, '{% trans "Edit manufacturer part" %}');
 | 
				
			||||||
 | 
					                    html += makeIconButton('fa-trash-alt icon-red', 'button-manufacturer-part-delete', pk, '{% trans "Delete manufacturer part" %}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    html += '</div>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return html;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 | 
					        onPostBody: function() {
 | 
				
			||||||
 | 
					            // Callbacks
 | 
				
			||||||
 | 
					            $(table).find('.button-manufacturer-part-edit').click(function() {
 | 
				
			||||||
 | 
					                var pk = $(this).attr('pk');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                editManufacturerPart(
 | 
				
			||||||
 | 
					                    pk, 
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        onSuccess: function() {
 | 
				
			||||||
 | 
					                            $(table).bootstrapTable('refresh');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $(table).find('.button-manufacturer-part-delete').click(function() {
 | 
				
			||||||
 | 
					                var pk = $(this).attr('pk');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                deleteManufacturerPart(
 | 
				
			||||||
 | 
					                    pk, 
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        onSuccess: function() {
 | 
				
			||||||
 | 
					                            $(table).bootstrapTable('refresh');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -570,7 +750,51 @@ function loadSupplierPartTable(table, url, options) {
 | 
				
			|||||||
                field: 'packaging',
 | 
					                field: 'packaging',
 | 
				
			||||||
                title: '{% trans "Packaging" %}',
 | 
					                title: '{% trans "Packaging" %}',
 | 
				
			||||||
                sortable: false,
 | 
					                sortable: false,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                field: 'actions',
 | 
				
			||||||
 | 
					                title: '',
 | 
				
			||||||
 | 
					                sortable: false,
 | 
				
			||||||
 | 
					                switchable: false,
 | 
				
			||||||
 | 
					                formatter: function(value, row) {
 | 
				
			||||||
 | 
					                    var pk = row.pk;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    var html = `<div class='btn-group float-right' role='group'>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    html += makeIconButton('fa-edit icon-blue', 'button-supplier-part-edit', pk, '{% trans "Edit supplier part" %}');
 | 
				
			||||||
 | 
					                    html += makeIconButton('fa-trash-alt icon-red', 'button-supplier-part-delete', pk, '{% trans "Delete supplier part" %}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    html += '</div>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return html;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 | 
					        onPostBody: function() {
 | 
				
			||||||
 | 
					            // Callbacks
 | 
				
			||||||
 | 
					            $(table).find('.button-supplier-part-edit').click(function() {
 | 
				
			||||||
 | 
					                var pk = $(this).attr('pk');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                editSupplierPart(
 | 
				
			||||||
 | 
					                    pk, 
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        onSuccess: function() {
 | 
				
			||||||
 | 
					                            $(table).bootstrapTable('refresh');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $(table).find('.button-supplier-part-delete').click(function() {
 | 
				
			||||||
 | 
					                var pk = $(this).attr('pk');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                deleteSupplierPart(
 | 
				
			||||||
 | 
					                    pk, 
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        onSuccess: function() {
 | 
				
			||||||
 | 
					                            $(table).bootstrapTable('refresh');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -252,6 +252,11 @@ function constructDeleteForm(fields, options) {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
function constructForm(url, options) {
 | 
					function constructForm(url, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // An "empty" form will be defined locally
 | 
				
			||||||
 | 
					    if (url == null) {
 | 
				
			||||||
 | 
					        constructFormBody({}, options);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Save the URL 
 | 
					    // Save the URL 
 | 
				
			||||||
    options.url = url;
 | 
					    options.url = url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -378,6 +383,11 @@ function constructFormBody(fields, options) {
 | 
				
			|||||||
                fields[field].placeholder = field_options.placeholder;
 | 
					                fields[field].placeholder = field_options.placeholder;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Choices
 | 
				
			||||||
 | 
					            if (field_options.choices) {
 | 
				
			||||||
 | 
					                fields[field].choices = field_options.choices;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Field prefix
 | 
					            // Field prefix
 | 
				
			||||||
            if (field_options.prefix) {
 | 
					            if (field_options.prefix) {
 | 
				
			||||||
                fields[field].prefix = field_options.prefix;
 | 
					                fields[field].prefix = field_options.prefix;
 | 
				
			||||||
@@ -1113,7 +1123,7 @@ function initializeRelatedField(name, field, options) {
 | 
				
			|||||||
        var pk = field.value;
 | 
					        var pk = field.value;
 | 
				
			||||||
        var url = `${field.api_url}/${pk}/`.replace('//', '/');
 | 
					        var url = `${field.api_url}/${pk}/`.replace('//', '/');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        inventreeGet(url, {}, {
 | 
					        inventreeGet(url, field.filters || {}, {
 | 
				
			||||||
            success: function(data) {
 | 
					            success: function(data) {
 | 
				
			||||||
                setRelatedFieldData(name, data, options);
 | 
					                setRelatedFieldData(name, data, options);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -1211,6 +1221,9 @@ function renderModelData(name, model, data, parameters, options) {
 | 
				
			|||||||
        case 'partparametertemplate':
 | 
					        case 'partparametertemplate':
 | 
				
			||||||
            renderer = renderPartParameterTemplate;
 | 
					            renderer = renderPartParameterTemplate;
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
 | 
					        case 'manufacturerpart':
 | 
				
			||||||
 | 
					            renderer = renderManufacturerPart;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
        case 'supplierpart':
 | 
					        case 'supplierpart':
 | 
				
			||||||
            renderer = renderSupplierPart;
 | 
					            renderer = renderSupplierPart;
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -765,6 +765,9 @@ function attachSecondaryModal(modal, options) {
 | 
				
			|||||||
function attachSecondaries(modal, secondaries) {
 | 
					function attachSecondaries(modal, secondaries) {
 | 
				
			||||||
    /* Attach a provided list of secondary modals */
 | 
					    /* Attach a provided list of secondary modals */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 2021-07-18 - Secondary modals will be disabled for now, until they are re-implemented in the "API forms" architecture
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (var i = 0; i < secondaries.length; i++) {
 | 
					    for (var i = 0; i < secondaries.length; i++) {
 | 
				
			||||||
        attachSecondaryModal(modal, secondaries[i]);
 | 
					        attachSecondaryModal(modal, secondaries[i]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,7 +67,9 @@ function renderStockItem(name, data, parameters, options) {
 | 
				
			|||||||
// Renderer for "StockLocation" model
 | 
					// Renderer for "StockLocation" model
 | 
				
			||||||
function renderStockLocation(name, data, parameters, options) {
 | 
					function renderStockLocation(name, data, parameters, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var html = `<span>${data.name}</span>`;
 | 
					    var level = '- '.repeat(data.level);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var html = `<span>${level}${data.pathstring}</span>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (data.description) {
 | 
					    if (data.description) {
 | 
				
			||||||
        html += ` - <i>${data.description}</i>`;
 | 
					        html += ` - <i>${data.description}</i>`;
 | 
				
			||||||
@@ -75,10 +77,6 @@ function renderStockLocation(name, data, parameters, options) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    html += `<span class='float-right'>{% trans "Location ID" %}: ${data.pk}</span>`;
 | 
					    html += `<span class='float-right'>{% trans "Location ID" %}: ${data.pk}</span>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (data.pathstring) {
 | 
					 | 
				
			||||||
        html += `<p><small>${data.pathstring}</small></p>`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html;
 | 
					    return html;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -154,7 +152,9 @@ function renderOwner(name, data, parameters, options) {
 | 
				
			|||||||
// Renderer for "PartCategory" model
 | 
					// Renderer for "PartCategory" model
 | 
				
			||||||
function renderPartCategory(name, data, parameters, options) {
 | 
					function renderPartCategory(name, data, parameters, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var html = `<span><b>${data.name}</b></span>`;
 | 
					    var level = '- '.repeat(data.level);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var html = `<span>${level}${data.pathstring}</span>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (data.description) {
 | 
					    if (data.description) {
 | 
				
			||||||
        html += ` - <i>${data.description}</i>`;
 | 
					        html += ` - <i>${data.description}</i>`;
 | 
				
			||||||
@@ -162,10 +162,6 @@ function renderPartCategory(name, data, parameters, options) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    html += `<span class='float-right'>{% trans "Category ID" %}: ${data.pk}</span>`;
 | 
					    html += `<span class='float-right'>{% trans "Category ID" %}: ${data.pk}</span>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (data.pathstring) {
 | 
					 | 
				
			||||||
        html += `<p><small>${data.pathstring}</small></p>`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html;
 | 
					    return html;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -178,7 +174,35 @@ function renderPartParameterTemplate(name, data, parameters, options) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Rendered for "SupplierPart" model
 | 
					// Renderer for "ManufacturerPart" model
 | 
				
			||||||
 | 
					function renderManufacturerPart(name, data, parameters, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var manufacturer_image = null;
 | 
				
			||||||
 | 
					    var part_image = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (data.manufacturer_detail) {
 | 
				
			||||||
 | 
					        manufacturer_image = data.manufacturer_detail.image;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (data.part_detail) {
 | 
				
			||||||
 | 
					        part_image = data.part_detail.thumbnail || data.part_detail.image;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var html = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    html += select2Thumbnail(manufacturer_image);
 | 
				
			||||||
 | 
					    html += select2Thumbnail(part_image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    html += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`;
 | 
				
			||||||
 | 
					    html += ` - <i>${data.part_detail.full_name}</i>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    html += `<span class='float-right'>{% trans "Manufacturer Part ID" %}: ${data.pk}</span>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Renderer for "SupplierPart" model
 | 
				
			||||||
function renderSupplierPart(name, data, parameters, options) {
 | 
					function renderSupplierPart(name, data, parameters, options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var supplier_image = null;
 | 
					    var supplier_image = null;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ function createSalesOrder(options={}) {
 | 
				
			|||||||
            customer: {
 | 
					            customer: {
 | 
				
			||||||
                value: options.customer,
 | 
					                value: options.customer,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            customer_reference: {},
 | 
				
			||||||
            description: {},
 | 
					            description: {},
 | 
				
			||||||
            target_date: {
 | 
					            target_date: {
 | 
				
			||||||
                icon: 'fa-calendar-alt',
 | 
					                icon: 'fa-calendar-alt',
 | 
				
			||||||
@@ -44,6 +45,7 @@ function createPurchaseOrder(options={}) {
 | 
				
			|||||||
            supplier: {
 | 
					            supplier: {
 | 
				
			||||||
                value: options.supplier,
 | 
					                value: options.supplier,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            supplier_reference: {},
 | 
				
			||||||
            description: {},
 | 
					            description: {},
 | 
				
			||||||
            target_date: {
 | 
					            target_date: {
 | 
				
			||||||
                icon: 'fa-calendar-alt',
 | 
					                icon: 'fa-calendar-alt',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,55 @@ function stockStatusCodes() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Export stock table
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function exportStock(params={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructFormBody({}, {
 | 
				
			||||||
 | 
					        title: '{% trans "Export Stock" %}',
 | 
				
			||||||
 | 
					        fields: {
 | 
				
			||||||
 | 
					            format: {
 | 
				
			||||||
 | 
					                label: '{% trans "Format" %}',
 | 
				
			||||||
 | 
					                help_text: '{% trans "Select file format" %}',
 | 
				
			||||||
 | 
					                required: true,
 | 
				
			||||||
 | 
					                type: 'choice',
 | 
				
			||||||
 | 
					                value: 'csv',
 | 
				
			||||||
 | 
					                choices: [
 | 
				
			||||||
 | 
					                    { value: 'csv', display_name: 'CSV' },
 | 
				
			||||||
 | 
					                    { value: 'tsv', display_name: 'TSV' },
 | 
				
			||||||
 | 
					                    { value: 'xls', display_name: 'XLS' },
 | 
				
			||||||
 | 
					                    { value: 'xlsx', display_name: 'XLSX' },
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            sublocations: {
 | 
				
			||||||
 | 
					                label: '{% trans "Include Sublocations" %}',
 | 
				
			||||||
 | 
					                help_text: '{% trans "Include stock items in sublocations" %}',
 | 
				
			||||||
 | 
					                type: 'boolean',
 | 
				
			||||||
 | 
					                value: 'true',
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onSubmit: function(fields, form_options) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var format = getFormFieldValue('format', fields['format'], form_options);
 | 
				
			||||||
 | 
					            var cascade = getFormFieldValue('sublocations', fields['sublocations'], form_options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Hide the modal
 | 
				
			||||||
 | 
					            $(form_options.modal).modal('hide');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var url = `{% url "stock-export" %}?format=${format}&cascade=${cascade}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (var key in params) {
 | 
				
			||||||
 | 
					                url += `&${key}=${params[key]}`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(url);
 | 
				
			||||||
 | 
					            location.href = url;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Perform stock adjustments
 | 
					 * Perform stock adjustments
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@@ -1616,27 +1665,6 @@ function createNewStockItem(options) {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    options.secondary = [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            field: 'part',
 | 
					 | 
				
			||||||
            label: '{% trans "New Part" %}',
 | 
					 | 
				
			||||||
            title: '{% trans "Create New Part" %}',
 | 
					 | 
				
			||||||
            url: "{% url 'part-create' %}",
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            field: 'supplier_part',
 | 
					 | 
				
			||||||
            label: '{% trans "New Supplier Part" %}',
 | 
					 | 
				
			||||||
            title: '{% trans "Create new Supplier Part" %}',
 | 
					 | 
				
			||||||
            url: "{% url 'supplier-part-create' %}"
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            field: 'location',
 | 
					 | 
				
			||||||
            label: '{% trans "New Location" %}',
 | 
					 | 
				
			||||||
            title: '{% trans "Create New Location" %}',
 | 
					 | 
				
			||||||
            url: "{% url 'stock-location-create' %}",
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    launchModalForm("{% url 'stock-item-create' %}", options);
 | 
					    launchModalForm("{% url 'stock-item-create' %}", options);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,11 @@
 | 
				
			|||||||
{% load i18n %}
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function reloadtable(table) {
 | 
				
			||||||
 | 
					    $(table).bootstrapTable('refresh');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function editButton(url, text='Edit') {
 | 
					function editButton(url, text='Edit') {
 | 
				
			||||||
    return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
 | 
					    return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user