2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-08 10:43:40 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters
2020-11-10 12:28:05 +11:00
17 changed files with 1447 additions and 895 deletions
+5
View File
@@ -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
+17 -4
View File
@@ -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>
+52 -2
View File
@@ -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):
@@ -270,6 +312,10 @@ class InvenTreeSetting(models.Model):
setting = InvenTreeSetting(key=key)
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)
+11 -1
View File
@@ -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
+5 -3
View File
@@ -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):
"""
+9 -4
View File
@@ -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',
]
@@ -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'),
),
]
+43 -7
View File
@@ -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
View 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 %}
+110
View File
@@ -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')
+9
View File
@@ -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" %}
+4 -2
View File
@@ -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}")