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: (
-
+ <>
+
+
+ >
)
},
{