mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Merge remote-tracking branch 'inventree/master'
This commit is contained in:
		@@ -28,6 +28,11 @@ class HelperForm(forms.ModelForm):
 | 
			
		||||
 | 
			
		||||
        self.helper.form_tag = False
 | 
			
		||||
 | 
			
		||||
        # Check for errors from model validation
 | 
			
		||||
        # If none, disable crispy form errors
 | 
			
		||||
        if not self.errors:
 | 
			
		||||
            self.helper.form_show_errors = False
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        Create a default 'layout' for this form.
 | 
			
		||||
        Ref: https://django-crispy-forms.readthedocs.io/en/latest/layouts.html
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,29 @@
 | 
			
		||||
    <li{% if tab == 'details' %} class='active'{% endif %}>
 | 
			
		||||
        <a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
 | 
			
		||||
    </li>
 | 
			
		||||
    {% if build.active %}
 | 
			
		||||
    <li{% if tab == 'allocate' %} class='active'{% endif %}>
 | 
			
		||||
        <a href="{% url 'build-allocate' build.id %}">{% trans "Allocate Parts" %}</a>
 | 
			
		||||
        <a href="{% url 'build-allocate' build.id %}">
 | 
			
		||||
            {% trans "Incomplete" %}
 | 
			
		||||
            <span class='badge'>{{ build.incomplete_outputs.count }}</span>
 | 
			
		||||
        </a>
 | 
			
		||||
    </li>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <li{% if tab == 'output' %} class='active'{% endif %}>
 | 
			
		||||
        <a href="{% url 'build-output' build.id %}">{% trans "Build Outputs" %}{% if build.output_count > 0%}<span class='badge'>{{ build.output_count }}</span>{% endif %}</a>
 | 
			
		||||
        <a href="{% url 'build-output' build.id %}">
 | 
			
		||||
            {% trans "Build Outputs" %}
 | 
			
		||||
            <span class='badge'>{{ build.output_count }}</span>
 | 
			
		||||
        </a>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li{% if tab == 'notes' %} class='active'{% endif %}>
 | 
			
		||||
        <a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='fas fa-info-circle'></span>{% endif %}</a>
 | 
			
		||||
        <a href="{% url 'build-notes' build.id %}">
 | 
			
		||||
            {% trans "Notes" %}
 | 
			
		||||
            {% if build.notes %} <span class='fas fa-info-circle'></span>{% endif %}
 | 
			
		||||
        </a>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li {% if tab == 'attachments' %} class='active'{% endif %}>
 | 
			
		||||
        <a href='{% url "build-attachments" build.id %}'>{% trans "Attachments" %}</a>
 | 
			
		||||
        <a href='{% url "build-attachments" build.id %}'>
 | 
			
		||||
            {% trans "Attachments" %}
 | 
			
		||||
        </a>
 | 
			
		||||
    </li>
 | 
			
		||||
</ul>
 | 
			
		||||
@@ -64,6 +64,13 @@ class InvenTreeSetting(models.Model):
 | 
			
		||||
            'description': _('Regular expression pattern for matching Part IPN')
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'PART_ALLOW_DUPLICATE_IPN': {
 | 
			
		||||
            'name': _('Allow Duplicate IPN'),
 | 
			
		||||
            'description': _('Allow multiple parts to share the same IPN'),
 | 
			
		||||
            'default': True,
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'PART_COPY_BOM': {
 | 
			
		||||
            'name': _('Copy Part BOM Data'),
 | 
			
		||||
            'description': _('Copy BOM data by default when duplicating a part'),
 | 
			
		||||
@@ -85,6 +92,34 @@ class InvenTreeSetting(models.Model):
 | 
			
		||||
            'validator': bool
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'PART_COMPONENT': {
 | 
			
		||||
            'name': _('Component'),
 | 
			
		||||
            'description': _('Parts can be used as sub-components by default'),
 | 
			
		||||
            'default': True,
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'PART_PURCHASEABLE': {
 | 
			
		||||
            'name': _('Purchaseable'),
 | 
			
		||||
            'description': _('Parts are purchaseable by default'),
 | 
			
		||||
            'default': False,
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'PART_SALABLE': {
 | 
			
		||||
            'name': _('Salable'),
 | 
			
		||||
            'description': _('Parts are salable by default'),
 | 
			
		||||
            'default': False,
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'PART_TRACKABLE': {
 | 
			
		||||
            'name': _('Trackable'),
 | 
			
		||||
            'description': _('Parts are trackable by default'),
 | 
			
		||||
            'default': False,
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'BUILDORDER_REFERENCE_PREFIX': {
 | 
			
		||||
            'name': _('Build Order Reference Prefix'),
 | 
			
		||||
            'description': _('Prefix value for build order reference'),
 | 
			
		||||
@@ -242,9 +277,16 @@ class InvenTreeSetting(models.Model):
 | 
			
		||||
        setting = InvenTreeSetting.get_setting_object(key)
 | 
			
		||||
 | 
			
		||||
        if setting:
 | 
			
		||||
            return setting.value
 | 
			
		||||
            value = setting.value
 | 
			
		||||
 | 
			
		||||
            # If the particular setting is defined as a boolean, cast the value to a boolean
 | 
			
		||||
            if setting.is_bool():
 | 
			
		||||
                value = InvenTree.helpers.str2bool(value)
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            return backup_value
 | 
			
		||||
            value = backup_value
 | 
			
		||||
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def set_setting(cls, key, value, user, create=True):
 | 
			
		||||
@@ -271,6 +313,10 @@ class InvenTreeSetting(models.Model):
 | 
			
		||||
            else:
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        # Enforce standard boolean representation
 | 
			
		||||
        if setting.is_bool():
 | 
			
		||||
            value = InvenTree.helpers.str2bool(value)
 | 
			
		||||
            
 | 
			
		||||
        setting.value = str(value)
 | 
			
		||||
        setting.save()
 | 
			
		||||
 | 
			
		||||
@@ -282,6 +328,10 @@ class InvenTreeSetting(models.Model):
 | 
			
		||||
    def name(self):
 | 
			
		||||
        return InvenTreeSetting.get_setting_name(self.key)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def default_value(self):
 | 
			
		||||
        return InvenTreeSetting.get_default_value(self.key)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self):
 | 
			
		||||
        return InvenTreeSetting.get_setting_description(self.key)
 | 
			
		||||
 
 | 
			
		||||
@@ -69,4 +69,14 @@ class SettingsTest(TestCase):
 | 
			
		||||
 | 
			
		||||
            InvenTreeSetting.set_setting(key, value, self.user)
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(str(value), InvenTreeSetting.get_setting(key))
 | 
			
		||||
            self.assertEqual(value, InvenTreeSetting.get_setting(key))
 | 
			
		||||
 | 
			
		||||
            # Any fields marked as 'boolean' must have a default value specified
 | 
			
		||||
            setting = InvenTreeSetting.get_setting_object(key)
 | 
			
		||||
 | 
			
		||||
            if setting.is_bool():
 | 
			
		||||
                if setting.default_value in ['', None]:
 | 
			
		||||
                    raise ValueError(f'Default value for boolean setting {key} not provided')
 | 
			
		||||
 | 
			
		||||
                if setting.default_value not in [True, False]:
 | 
			
		||||
                    raise ValueError(f'Non-boolean default value specified for {key}')
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -335,7 +335,8 @@ class PurchaseOrderCreate(AjaxCreateView):
 | 
			
		||||
 | 
			
		||||
        order = form.save(commit=False)
 | 
			
		||||
        order.created_by = self.request.user
 | 
			
		||||
        order.save()
 | 
			
		||||
        
 | 
			
		||||
        return super().save(form)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SalesOrderCreate(AjaxCreateView):
 | 
			
		||||
@@ -370,7 +371,8 @@ class SalesOrderCreate(AjaxCreateView):
 | 
			
		||||
 | 
			
		||||
        order = form.save(commit=False)
 | 
			
		||||
        order.created_by = self.request.user
 | 
			
		||||
        order.save()
 | 
			
		||||
        
 | 
			
		||||
        return super().save(form)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PurchaseOrderEdit(AjaxUpdateView):
 | 
			
		||||
@@ -428,7 +430,7 @@ class PurchaseOrderCancel(AjaxUpdateView):
 | 
			
		||||
            form.add_error('confirm', _('Confirm order cancellation'))
 | 
			
		||||
 | 
			
		||||
        if not order.can_cancel():
 | 
			
		||||
            form.add_error(None, _('Order cannot be cancelled'))
 | 
			
		||||
            form.add_error(None, _('Order cannot be cancelled as either pending or placed'))
 | 
			
		||||
 | 
			
		||||
    def save(self, order, form, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
 
 | 
			
		||||
@@ -174,7 +174,9 @@ class SetPartCategoryForm(forms.Form):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditPartForm(HelperForm):
 | 
			
		||||
    """ Form for editing a Part object """
 | 
			
		||||
    """
 | 
			
		||||
    Form for editing a Part object.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    field_prefix = {
 | 
			
		||||
        'keywords': 'fa-key',
 | 
			
		||||
@@ -202,14 +204,14 @@ class EditPartForm(HelperForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Part
 | 
			
		||||
        fields = [
 | 
			
		||||
            'bom_copy',
 | 
			
		||||
            'parameters_copy',
 | 
			
		||||
            'confirm_creation',
 | 
			
		||||
            'category',
 | 
			
		||||
            'name',
 | 
			
		||||
            'IPN',
 | 
			
		||||
            'description',
 | 
			
		||||
            'revision',
 | 
			
		||||
            'bom_copy',
 | 
			
		||||
            'parameters_copy',
 | 
			
		||||
            'confirm_creation',
 | 
			
		||||
            'keywords',
 | 
			
		||||
            'variant_of',
 | 
			
		||||
            'link',
 | 
			
		||||
@@ -217,6 +219,9 @@ class EditPartForm(HelperForm):
 | 
			
		||||
            'default_supplier',
 | 
			
		||||
            'units',
 | 
			
		||||
            'minimum_stock',
 | 
			
		||||
            'trackable',
 | 
			
		||||
            'purchaseable',
 | 
			
		||||
            'salable',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								InvenTree/part/migrations/0054_auto_20201109_1246.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								InvenTree/part/migrations/0054_auto_20201109_1246.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
# Generated by Django 3.0.7 on 2020-11-09 12:46
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import part.settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('part', '0053_merge_20201103_1028'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='active',
 | 
			
		||||
            field=models.BooleanField(default=True, help_text='Is this part active?', verbose_name='Active'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='component',
 | 
			
		||||
            field=models.BooleanField(default=part.settings.part_component_default, help_text='Can this part be used to build other parts?', verbose_name='Component'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='purchaseable',
 | 
			
		||||
            field=models.BooleanField(default=part.settings.part_purchaseable_default, help_text='Can this part be purchased from external suppliers?', verbose_name='Purchaseable'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='salable',
 | 
			
		||||
            field=models.BooleanField(default=part.settings.part_salable_default, help_text='Can this part be sold to customers?', verbose_name='Salable'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='trackable',
 | 
			
		||||
            field=models.BooleanField(default=part.settings.part_trackable_default, help_text='Does this part have tracking for unique items?', verbose_name='Trackable'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='virtual',
 | 
			
		||||
            field=models.BooleanField(default=False, help_text='Is this a virtual part, such as a software product or license?', verbose_name='Virtual'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -47,6 +47,7 @@ from company.models import SupplierPart
 | 
			
		||||
from stock import models as StockModels
 | 
			
		||||
 | 
			
		||||
import common.models
 | 
			
		||||
import part.settings as part_settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartCategory(InvenTreeTree):
 | 
			
		||||
@@ -528,6 +529,18 @@ class Part(MPTTModel):
 | 
			
		||||
        """
 | 
			
		||||
        super().validate_unique(exclude)
 | 
			
		||||
 | 
			
		||||
        # User can decide whether duplicate IPN (Internal Part Number) values are allowed
 | 
			
		||||
        allow_duplicate_ipn = common.models.InvenTreeSetting.get_setting('PART_ALLOW_DUPLICATE_IPN')
 | 
			
		||||
 | 
			
		||||
        if not allow_duplicate_ipn:
 | 
			
		||||
            parts = Part.objects.filter(IPN__iexact=self.IPN)
 | 
			
		||||
            parts = parts.exclude(pk=self.pk)
 | 
			
		||||
 | 
			
		||||
            if parts.exists():
 | 
			
		||||
                raise ValidationError({
 | 
			
		||||
                    'IPN': _('Duplicate IPN not allowed in part settings'),
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        # Part name uniqueness should be case insensitive
 | 
			
		||||
        try:
 | 
			
		||||
            parts = Part.objects.exclude(id=self.id).filter(
 | 
			
		||||
@@ -656,19 +669,42 @@ class Part(MPTTModel):
 | 
			
		||||
 | 
			
		||||
    units = models.CharField(max_length=20, default="", blank=True, null=True, help_text=_('Stock keeping units for this part'))
 | 
			
		||||
 | 
			
		||||
    assembly = models.BooleanField(default=False, verbose_name='Assembly', help_text=_('Can this part be built from other parts?'))
 | 
			
		||||
    assembly = models.BooleanField(
 | 
			
		||||
        default=False,
 | 
			
		||||
        verbose_name=_('Assembly'),
 | 
			
		||||
        help_text=_('Can this part be built from other parts?')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    component = models.BooleanField(default=True, verbose_name='Component', help_text=_('Can this part be used to build other parts?'))
 | 
			
		||||
    component = models.BooleanField(
 | 
			
		||||
        default=part_settings.part_component_default,
 | 
			
		||||
        verbose_name=_('Component'),
 | 
			
		||||
        help_text=_('Can this part be used to build other parts?')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    trackable = models.BooleanField(default=False, help_text=_('Does this part have tracking for unique items?'))
 | 
			
		||||
    trackable = models.BooleanField(
 | 
			
		||||
        default=part_settings.part_trackable_default,
 | 
			
		||||
        verbose_name=_('Trackable'),
 | 
			
		||||
        help_text=_('Does this part have tracking for unique items?'))
 | 
			
		||||
 | 
			
		||||
    purchaseable = models.BooleanField(default=True, help_text=_('Can this part be purchased from external suppliers?'))
 | 
			
		||||
    purchaseable = models.BooleanField(
 | 
			
		||||
        default=part_settings.part_purchaseable_default,
 | 
			
		||||
        verbose_name=_('Purchaseable'),
 | 
			
		||||
        help_text=_('Can this part be purchased from external suppliers?'))
 | 
			
		||||
 | 
			
		||||
    salable = models.BooleanField(default=False, help_text=_("Can this part be sold to customers?"))
 | 
			
		||||
    salable = models.BooleanField(
 | 
			
		||||
        default=part_settings.part_salable_default,
 | 
			
		||||
        verbose_name=_('Salable'),
 | 
			
		||||
        help_text=_("Can this part be sold to customers?"))
 | 
			
		||||
 | 
			
		||||
    active = models.BooleanField(default=True, help_text=_('Is this part active?'))
 | 
			
		||||
    active = models.BooleanField(
 | 
			
		||||
        default=True,
 | 
			
		||||
        verbose_name=_('Active'),
 | 
			
		||||
        help_text=_('Is this part active?'))
 | 
			
		||||
 | 
			
		||||
    virtual = models.BooleanField(default=False, help_text=_('Is this a virtual part, such as a software product or license?'))
 | 
			
		||||
    virtual = models.BooleanField(
 | 
			
		||||
        default=False,
 | 
			
		||||
        verbose_name=_('Virtual'),
 | 
			
		||||
        help_text=_('Is this a virtual part, such as a software product or license?'))
 | 
			
		||||
 | 
			
		||||
    notes = MarkdownxField(blank=True, null=True, help_text=_('Part notes - supports Markdown formatting'))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								InvenTree/part/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								InvenTree/part/settings.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
"""
 | 
			
		||||
User-configurable settings for the Part app
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def part_component_default():
 | 
			
		||||
    """
 | 
			
		||||
    Returns the default value for the 'component' field of a Part object
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    return InvenTreeSetting.get_setting('PART_COMPONENT')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def part_purchaseable_default():
 | 
			
		||||
    """
 | 
			
		||||
    Returns the default value for the 'purchasable' field for a Part object
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    return InvenTreeSetting.get_setting('PART_PURCHASEABLE')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def part_salable_default():
 | 
			
		||||
    """
 | 
			
		||||
    Returns the default value for the 'salable' field for a Part object
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    return InvenTreeSetting.get_setting('PART_SALABLE')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def part_trackable_default():
 | 
			
		||||
    """
 | 
			
		||||
    Returns the defualt value fro the 'trackable' field for a Part object
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    return InvenTreeSetting.get_setting('PART_TRACKABLE')
 | 
			
		||||
@@ -203,6 +203,7 @@
 | 
			
		||||
            <td><i>{% trans "Part cannot be sold to customers" %}</i></td>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </tr>
 | 
			
		||||
        <tr><td colspan='4'></td></tr>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>
 | 
			
		||||
                {% if part.active %}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@
 | 
			
		||||
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
 | 
			
		||||
@@ -13,6 +15,10 @@ from .models import Part, PartTestTemplate
 | 
			
		||||
from .models import rename_part_image, match_part_names
 | 
			
		||||
from .templatetags import inventree_extras
 | 
			
		||||
 | 
			
		||||
import part.settings
 | 
			
		||||
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemplateTagTest(TestCase):
 | 
			
		||||
    """ Tests for the custom template tag code """
 | 
			
		||||
@@ -164,3 +170,107 @@ class TestTemplateTest(TestCase):
 | 
			
		||||
        PartTestTemplate.objects.create(part=variant, test_name='A Sample Test')
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(variant.getTestTemplates().count(), n + 1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartSettingsTest(TestCase):
 | 
			
		||||
    """
 | 
			
		||||
    Tests to ensure that the user-configurable default values work as expected.
 | 
			
		||||
 | 
			
		||||
    Some fields for the Part model can have default values specified by the user.
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # Create a user for auth
 | 
			
		||||
        User = get_user_model()
 | 
			
		||||
 | 
			
		||||
        self.user = User.objects.create_user(
 | 
			
		||||
            username='testuser',
 | 
			
		||||
            email='test@testing.com',
 | 
			
		||||
            password='password',
 | 
			
		||||
            is_staff=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def make_part(self):
 | 
			
		||||
        """
 | 
			
		||||
        Helper function to create a simple part
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        part = Part.objects.create(
 | 
			
		||||
            name='Test Part',
 | 
			
		||||
            description='I am but a humble test part',
 | 
			
		||||
            IPN='IPN-123',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return part
 | 
			
		||||
 | 
			
		||||
    def test_defaults(self):
 | 
			
		||||
        """
 | 
			
		||||
        Test that the default values for the part settings are correct
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(part.settings.part_component_default())
 | 
			
		||||
        self.assertFalse(part.settings.part_purchaseable_default())
 | 
			
		||||
        self.assertFalse(part.settings.part_salable_default())
 | 
			
		||||
        self.assertFalse(part.settings.part_trackable_default())
 | 
			
		||||
 | 
			
		||||
    def test_initial(self):
 | 
			
		||||
        """
 | 
			
		||||
        Test the 'initial' default values (no default values have been set)
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        part = self.make_part()
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(part.component)
 | 
			
		||||
        self.assertFalse(part.purchaseable)
 | 
			
		||||
        self.assertFalse(part.salable)
 | 
			
		||||
        self.assertFalse(part.trackable)
 | 
			
		||||
 | 
			
		||||
    def test_custom(self):
 | 
			
		||||
        """
 | 
			
		||||
        Update some of the part values and re-test
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        for val in [True, False]:
 | 
			
		||||
            InvenTreeSetting.set_setting('PART_COMPONENT', val, self.user)
 | 
			
		||||
            InvenTreeSetting.set_setting('PART_PURCHASEABLE', val, self.user)
 | 
			
		||||
            InvenTreeSetting.set_setting('PART_SALABLE', val, self.user)
 | 
			
		||||
            InvenTreeSetting.set_setting('PART_TRACKABLE', val, self.user)
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(val, InvenTreeSetting.get_setting('PART_COMPONENT'))
 | 
			
		||||
            self.assertEqual(val, InvenTreeSetting.get_setting('PART_PURCHASEABLE'))
 | 
			
		||||
            self.assertEqual(val, InvenTreeSetting.get_setting('PART_SALABLE'))
 | 
			
		||||
            self.assertEqual(val, InvenTreeSetting.get_setting('PART_TRACKABLE'))
 | 
			
		||||
 | 
			
		||||
            part = self.make_part()
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(part.component, val)
 | 
			
		||||
            self.assertEqual(part.purchaseable, val)
 | 
			
		||||
            self.assertEqual(part.salable, val)
 | 
			
		||||
            self.assertEqual(part.trackable, val)
 | 
			
		||||
    
 | 
			
		||||
            Part.objects.filter(pk=part.pk).delete()
 | 
			
		||||
 | 
			
		||||
    def test_duplicate_ipn(self):
 | 
			
		||||
        """
 | 
			
		||||
        Test the setting which controls duplicate IPN values
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # Create a part
 | 
			
		||||
        Part.objects.create(name='Hello', description='A thing', IPN='IPN123')
 | 
			
		||||
 | 
			
		||||
        # Attempt to create a duplicate item (should fail)
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            Part.objects.create(name='Hello', description='A thing', IPN='IPN123')
 | 
			
		||||
 | 
			
		||||
        # Attempt to create item with duplicate IPN (should be allowed by default)
 | 
			
		||||
        Part.objects.create(name='Hello', description='A thing', IPN='IPN123', revision='B')
 | 
			
		||||
 | 
			
		||||
        # And attempt again with the same values (should fail)
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            Part.objects.create(name='Hello', description='A thing', IPN='IPN123', revision='B')
 | 
			
		||||
 | 
			
		||||
        # Now update the settings so duplicate IPN values are *not* allowed
 | 
			
		||||
        InvenTreeSetting.set_setting('PART_ALLOW_DUPLICATE_IPN', False, self.user)
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            Part.objects.create(name='Hello', description='A thing', IPN='IPN123', revision='C')
 | 
			
		||||
 
 | 
			
		||||
@@ -1247,12 +1247,21 @@ class StockItem(MPTTModel):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def required_test_count(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return the number of 'required tests' for this StockItem
 | 
			
		||||
        """
 | 
			
		||||
        return self.part.getRequiredTests().count()
 | 
			
		||||
 | 
			
		||||
    def hasRequiredTests(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return True if there are any 'required tests' associated with this StockItem
 | 
			
		||||
        """
 | 
			
		||||
        return self.part.getRequiredTests().count() > 0
 | 
			
		||||
 | 
			
		||||
    def passedAllRequiredTests(self):
 | 
			
		||||
        """
 | 
			
		||||
        Returns True if this StockItem has passed all required tests
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        status = self.requiredTestStatus()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,19 @@
 | 
			
		||||
 | 
			
		||||
{% block settings %}
 | 
			
		||||
 | 
			
		||||
<h4>{% trans "Part Options" %}</h4>
 | 
			
		||||
 | 
			
		||||
<table class='table table-striped table-condensed'>
 | 
			
		||||
    <thead></thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
 | 
			
		||||
        <tr><td colspan='4'></td></tr>
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_COMPONENT" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_PURCHASEABLE" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_SALABLE" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_TRACKABLE" %}
 | 
			
		||||
        <tr><td colspan='4'></td></tr>
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="PART_COPY_TESTS" %}
 | 
			
		||||
 
 | 
			
		||||
@@ -316,7 +316,8 @@ def update_group_roles(group, debug=False):
 | 
			
		||||
 | 
			
		||||
        permission = get_permission_object(perm)
 | 
			
		||||
 | 
			
		||||
        group.permissions.add(permission)
 | 
			
		||||
        if permission:
 | 
			
		||||
            group.permissions.add(permission)
 | 
			
		||||
 | 
			
		||||
        if debug:
 | 
			
		||||
            print(f"Adding permission {perm} to group {group.name}")
 | 
			
		||||
@@ -330,7 +331,8 @@ def update_group_roles(group, debug=False):
 | 
			
		||||
 | 
			
		||||
        permission = get_permission_object(perm)
 | 
			
		||||
 | 
			
		||||
        group.permissions.remove(permission)
 | 
			
		||||
        if permission:
 | 
			
		||||
            group.permissions.remove(permission)
 | 
			
		||||
 | 
			
		||||
        if debug:
 | 
			
		||||
            print(f"Removing permission {perm} from group {group.name}")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user