2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-13 21:17:33 +00:00

External order checks (#11935)

* Add new global settings

Co-authored-by: Copilot <copilot@github.com>

* Validation logic

Co-authored-by: Copilot <copilot@github.com>

* Remove one setting

- Already covered if build order requires validation

* Add unit test

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Oliver
2026-05-13 15:16:34 +10:00
committed by GitHub
parent 5d72eb4f1d
commit 727ca62883
5 changed files with 91 additions and 16 deletions
+2 -1
View File
@@ -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") }}
+45 -1
View File
@@ -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
@@ -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': _(
+15 -1
View File
@@ -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(
@@ -290,19 +290,27 @@ export default function SystemSettings() {
label: t`Manufacturing`,
icon: <IconBuildingFactory2 />,
content: (
<GlobalSettingList
heading={t`Build Orders`}
keys={[
'BUILDORDER_REFERENCE_PATTERN',
'BUILDORDER_EXTERNAL_BUILDS',
'BUILDORDER_REQUIRE_RESPONSIBLE',
'BUILDORDER_REQUIRE_ACTIVE_PART',
'BUILDORDER_REQUIRE_LOCKED_PART',
'BUILDORDER_REQUIRE_VALID_BOM',
'BUILDORDER_REQUIRE_CLOSED_CHILDS',
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS'
]}
/>
<>
<GlobalSettingList
heading={t`Build Orders`}
keys={[
'BUILDORDER_REFERENCE_PATTERN',
'BUILDORDER_REQUIRE_RESPONSIBLE',
'BUILDORDER_REQUIRE_ACTIVE_PART',
'BUILDORDER_REQUIRE_LOCKED_PART',
'BUILDORDER_REQUIRE_VALID_BOM',
'BUILDORDER_REQUIRE_CLOSED_CHILDS',
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS'
]}
/>
<GlobalSettingList
heading={t`External Build Orders`}
keys={[
'BUILDORDER_EXTERNAL_BUILDS',
'BUILDORDER_EXTERNAL_REQUIRED'
]}
/>
</>
)
},
{