diff --git a/docs/docs/manufacturing/build.md b/docs/docs/manufacturing/build.md index 3a8b5b8fc8..496e2c1dde 100644 --- a/docs/docs/manufacturing/build.md +++ b/docs/docs/manufacturing/build.md @@ -304,10 +304,11 @@ The following [global settings](../settings/global.md) are available for adjusti | Name | Description | Default | Units | | ---- | ----------- | ------- | ----- | {{ globalsetting("BUILDORDER_REFERENCE_PATTERN") }} -{{ globalsetting("BUILDORDER_EXTERNAL_BUILDS") }} {{ globalsetting("BUILDORDER_REQUIRE_RESPONSIBLE") }} {{ globalsetting("BUILDORDER_REQUIRE_ACTIVE_PART") }} {{ globalsetting("BUILDORDER_REQUIRE_LOCKED_PART") }} {{ globalsetting("BUILDORDER_REQUIRE_VALID_BOM") }} {{ globalsetting("BUILDORDER_REQUIRE_CLOSED_CHILDS") }} {{ globalsetting("PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS") }} +{{ globalsetting("BUILDORDER_EXTERNAL_BUILDS") }} +{{ globalsetting("BUILDORDER_EXTERNAL_REQUIRED") }} diff --git a/src/backend/InvenTree/build/test_build.py b/src/backend/InvenTree/build/test_build.py index 608bff91b4..bb1007dff2 100644 --- a/src/backend/InvenTree/build/test_build.py +++ b/src/backend/InvenTree/build/test_build.py @@ -932,7 +932,10 @@ class ExternalBuildTest(InvenTreeAPITestCase): def test_validation(self): """Test validation of external build logic.""" part = Part.objects.create( - name='Test part', description='A test part', purchaseable=False + name='Test part', + description='A test part', + assembly=True, + purchaseable=False, ) # Create a build order @@ -949,6 +952,47 @@ class ExternalBuildTest(InvenTreeAPITestCase): str(err.exception.messages), ) + def test_build_requirement(self): + """Test the global 'BUILDORDER_EXTERNAL_REQUIRED' setting.""" + # Create required test data + part = Part.objects.create( + name='Test part', + description='A test part', + assembly=True, + purchaseable=True, + ) + supplier = company.models.Company.objects.create( + name='Test supplier', active=True, is_supplier=True + ) + supplier_part = company.models.SupplierPart.objects.create( + part=part, supplier=supplier, SKU='TEST-123' + ) + + po = PurchaseOrder.objects.create(supplier=supplier, reference='PO-9999') + po_line = PurchaseOrderLineItem.objects.create( + order=po, part=supplier_part, quantity=10 + ) + + set_global_setting('BUILDORDER_EXTERNAL_REQUIRED', False) + po_line.clean() # Should not raise an error + + set_global_setting('BUILDORDER_EXTERNAL_BUILDS', True) + set_global_setting('BUILDORDER_EXTERNAL_REQUIRED', True) + + # Expect failure, there is no linked build order + with self.assertRaises(ValidationError): + po_line.clean() + + # Create and link a build order + build = Build.objects.create( + part=part, title='Test build order', quantity=10, external=True + ) + po_line.build_order = build + po_line.save() + + # Clean step now passes + po_line.clean() + def test_logic(self): """Test external build logic.""" # Create a purchaseable assembly part diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index 395794b3de..513d579f78 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -832,6 +832,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, + 'BUILDORDER_EXTERNAL_REQUIRED': { + 'name': _('Require External Build Orders'), + 'description': _( + 'Require an external build order when ordering assembled parts from an external supplier' + ), + 'default': False, + 'validator': bool, + }, 'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS': { 'name': _('Block Until Tests Pass'), 'description': _( diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 589291b2e7..c597e8c011 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -1955,13 +1955,16 @@ class PurchaseOrderLineItem(OrderLineItem): if self.part.supplier != self.order.supplier: raise ValidationError({'part': _('Supplier part must match supplier')}) + # Link to the base part + part = self.part.part + if self.build_order: if not self.build_order.external: raise ValidationError({ 'build_order': _('Build order must be marked as external') }) - if part := self.part.part: + if part: if not part.assembly: raise ValidationError({ 'build_order': _( @@ -1974,6 +1977,17 @@ class PurchaseOrderLineItem(OrderLineItem): 'build_order': _('Build order part must match line item part') }) + # Extra checks for external builds + if part and part.assembly and get_global_setting('BUILDORDER_EXTERNAL_BUILDS'): + if not self.build_order and get_global_setting( + 'BUILDORDER_EXTERNAL_REQUIRED' + ): + raise ValidationError({ + 'build_order': _( + 'An external build order is required for assembly parts' + ) + }) + def __str__(self): """Render a string representation of a PurchaseOrderLineItem instance.""" return '{n} x {part} - {po}'.format( diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index a8532c9c89..e03660fe59 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -290,19 +290,27 @@ export default function SystemSettings() { label: t`Manufacturing`, icon: , content: ( - + <> + + + ) }, {