diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 7430eb4a29..b21d2952b2 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -120,8 +120,23 @@ class Build( self.validate_reference_field(self.reference) self.reference_int = self.rebuild_reference_field(self.reference) + if get_global_setting('BUILDORDER_REQUIRE_VALID_BOM'): + # Check that the BOM is valid + if not self.part.is_bom_valid(): + raise ValidationError({ + 'part': _('Assembly BOM has not been validated') + }) + + if get_global_setting('BUILDORDER_REQUIRE_ACTIVE_PART'): + # Check that the part is active + if not self.part.active: + raise ValidationError({ + 'part': _('Part is not active') + }) + # On first save (i.e. creation), run some extra checks if self.pk is None: + # Set the destination location (if not specified) if not self.destination: self.destination = self.part.get_default_location() diff --git a/src/backend/InvenTree/build/tests.py b/src/backend/InvenTree/build/tests.py index 4dd7ee0fee..107e165a6c 100644 --- a/src/backend/InvenTree/build/tests.py +++ b/src/backend/InvenTree/build/tests.py @@ -1,6 +1,7 @@ """Basic unit tests for the BuildOrder app""" from django.conf import settings +from django.core.exceptions import ValidationError from django.test import tag from django.urls import reverse @@ -9,8 +10,10 @@ from datetime import datetime, timedelta from InvenTree.unit_test import InvenTreeTestCase from .models import Build +from part.models import Part, BomItem from stock.models import StockItem +from common.settings import get_global_setting, set_global_setting from build.status_codes import BuildStatus @@ -88,6 +91,64 @@ class BuildTestSimple(InvenTreeTestCase): self.assertEqual(build.status, BuildStatus.CANCELLED) + def test_build_create(self): + """Test creation of build orders via API.""" + + n = Build.objects.count() + + # Find an assembly part + assembly = Part.objects.filter(assembly=True).first() + + self.assertEqual(assembly.get_bom_items().count(), 0) + + # Let's create some BOM items for this assembly + for component in Part.objects.filter(assembly=False, component=True)[:15]: + + try: + BomItem.objects.create( + part=assembly, + sub_part=component, + reference='xxx', + quantity=5 + ) + except ValidationError: + pass + + # The assembly has a BOM, and is now *invalid* + self.assertGreater(assembly.get_bom_items().count(), 0) + self.assertFalse(assembly.is_bom_valid()) + + # Create a build for an assembly with an *invalid* BOM + set_global_setting('BUILDORDER_REQUIRE_VALID_BOM', False) + set_global_setting('BUILDORDER_REQUIRE_ACTIVE_PART', True) + + bo = Build.objects.create(part=assembly, quantity=10, reference='BO-9990') + bo.save() + + # Now, require a *valid* BOM + set_global_setting('BUILDORDER_REQUIRE_VALID_BOM', True) + + with self.assertRaises(ValidationError): + bo = Build.objects.create(part=assembly, quantity=10, reference='BO-9991') + + # Now, validate the BOM, and try again + assembly.validate_bom(None) + self.assertTrue(assembly.is_bom_valid()) + + bo = Build.objects.create(part=assembly, quantity=10, reference='BO-9992') + + # Now, try and create a build for an inactive assembly + assembly.active = False + assembly.save() + + with self.assertRaises(ValidationError): + bo = Build.objects.create(part=assembly, quantity=10, reference='BO-9993') + + set_global_setting('BUILDORDER_REQUIRE_ACTIVE_PART', False) + Build.objects.create(part=assembly, quantity=10, reference='BO-9994') + + # Check that expected quantity of new builds is created + self.assertEqual(Build.objects.count(), n + 3) class TestBuildViews(InvenTreeTestCase): """Tests for Build app views.""" diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index ceb5e2dcb9..b9962e9109 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -1786,6 +1786,20 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'default': False, 'validator': bool, }, + 'BUILDORDER_REQUIRE_ACTIVE_PART': { + 'name': _('Require Active Part'), + 'description': _('Prevent build order creation for inactive parts'), + 'default': False, + 'validator': bool, + }, + 'BUILDORDER_REQUIRE_VALID_BOM': { + 'name': _('Require Valid BOM'), + 'description': _( + 'Prevent build order creation unless BOM has been validated' + ), + 'default': False, + 'validator': bool, + }, 'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS': { 'name': _('Block Until Tests Pass'), 'description': _( diff --git a/src/backend/InvenTree/templates/InvenTree/settings/build.html b/src/backend/InvenTree/templates/InvenTree/settings/build.html index 781bb71525..e3029a3278 100644 --- a/src/backend/InvenTree/templates/InvenTree/settings/build.html +++ b/src/backend/InvenTree/templates/InvenTree/settings/build.html @@ -14,6 +14,8 @@
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PATTERN" %} {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REQUIRE_RESPONSIBLE" %} + {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REQUIRE_ACTIVE_PART" %} + {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REQUIRE_VALID_BOM" %} {% include "InvenTree/settings/setting.html" with key="PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS" %} diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index 69d5ef6953..0492d6e7ad 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -245,6 +245,8 @@ export default function SystemSettings() { keys={[ 'BUILDORDER_REFERENCE_PATTERN', 'BUILDORDER_REQUIRE_RESPONSIBLE', + 'BUILDORDER_REQUIRE_ACTIVE_PART', + 'BUILDORDER_REQUIRE_VALID_BOM', 'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS' ]} />