Skip to content

Commit

Permalink
Add option to disable the build output completion if are tests not pa…
Browse files Browse the repository at this point in the history
…ssed (#6057)

* Add option to disable the build output completion if required tests not passed

Fixes #5037

* Fix review comments

* Added tests

* Add settinsg option to PUI

* Utilize F" string concatenation

* Add validation to serializer too to being able to generate proper error message in the case if multiple outputs having incomplete tests

* Fix other build tests failing because of the new stock items

* Remove len from array empty check

* Update serializers.py

* Update models.py

Simplify error message

* Update settings.py

Formatting fix

* Update models.py

More style fixes

* Update models.py

Remove empty line

---------

Co-authored-by: Oliver <[email protected]>
  • Loading branch information
martonmiklos and SchrodingersGat authored Feb 17, 2024
1 parent 7adf2e0 commit ad1c1ae
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 6 deletions.
5 changes: 5 additions & 0 deletions InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,11 @@ def complete_build_output(self, output, user, **kwargs):
# List the allocated BuildItem objects for the given output
allocated_items = output.items_to_install.all()

if (common.settings.prevent_build_output_complete_on_incompleted_tests() and output.hasRequiredTests() and not output.passedAllRequiredTests()):
serial = output.serial
raise ValidationError(
_(f"Build output {serial} has not passed all required tests"))

for build_item in allocated_items:
# Complete the allocation of stock for that item
build_item.complete_allocation(user)
Expand Down
12 changes: 12 additions & 0 deletions InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from stock.models import generate_batch_code, StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer

import common.models
from common.serializers import ProjectCodeSerializer
import part.filters
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
Expand Down Expand Up @@ -523,6 +524,17 @@ def validate(self, data):

outputs = data.get('outputs', [])

if common.settings.prevent_build_output_complete_on_incompleted_tests():
errors = []
for output in outputs:
stock_item = output['output']
if stock_item.hasRequiredTests() and not stock_item.passedAllRequiredTests():
serial = stock_item.serial
errors.append(_(f"Build output {serial} has not passed all required tests"))

if errors:
raise ValidationError(errors)

if len(outputs) == 0:
raise ValidationError(_("A list of build outputs must be provided"))

Expand Down
100 changes: 95 additions & 5 deletions InvenTree/build/test_build.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Unit tests for the 'build' models"""

import uuid
from datetime import datetime, timedelta

from django.test import TestCase
Expand All @@ -14,8 +14,8 @@
import common.models
import build.tasks
from build.models import Build, BuildItem, BuildLine, generate_next_build_reference
from part.models import Part, BomItem, BomItemSubstitute
from stock.models import StockItem
from part.models import Part, BomItem, BomItemSubstitute, PartTestTemplate
from stock.models import StockItem, StockItemTestResult
from users.models import Owner

import logging
Expand Down Expand Up @@ -55,6 +55,76 @@ def setUpTestData(cls):
trackable=True,
)

# create one build with one required test template
cls.tested_part_with_required_test = Part.objects.create(
name="Part having required tests",
description="Why does it matter what my description is?",
assembly=True,
trackable=True,
)

cls.test_template_required = PartTestTemplate.objects.create(
part=cls.tested_part_with_required_test,
test_name="Required test",
description="Required test template description",
required=True,
requires_value=False,
requires_attachment=False
)

ref = generate_next_build_reference()

cls.build_w_tests_trackable = Build.objects.create(
reference=ref,
title="This is a build",
part=cls.tested_part_with_required_test,
quantity=1,
issued_by=get_user_model().objects.get(pk=1),
)

cls.stockitem_with_required_test = StockItem.objects.create(
part=cls.tested_part_with_required_test,
quantity=1,
is_building=True,
serial=uuid.uuid4(),
build=cls.build_w_tests_trackable
)

# now create a part with a non-required test template
cls.tested_part_wo_required_test = Part.objects.create(
name="Part with one non.required test",
description="Why does it matter what my description is?",
assembly=True,
trackable=True,
)

cls.test_template_non_required = PartTestTemplate.objects.create(
part=cls.tested_part_wo_required_test,
test_name="Required test template",
description="Required test template description",
required=False,
requires_value=False,
requires_attachment=False
)

ref = generate_next_build_reference()

cls.build_wo_tests_trackable = Build.objects.create(
reference=ref,
title="This is a build",
part=cls.tested_part_wo_required_test,
quantity=1,
issued_by=get_user_model().objects.get(pk=1),
)

cls.stockitem_wo_required_test = StockItem.objects.create(
part=cls.tested_part_wo_required_test,
quantity=1,
is_building=True,
serial=uuid.uuid4(),
build=cls.build_wo_tests_trackable
)

cls.sub_part_1 = Part.objects.create(
name="Widget A",
description="A widget",
Expand Down Expand Up @@ -245,7 +315,7 @@ def test_next_ref(self):

def test_init(self):
"""Perform some basic tests before we start the ball rolling"""
self.assertEqual(StockItem.objects.count(), 10)
self.assertEqual(StockItem.objects.count(), 12)

# Build is PENDING
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
Expand Down Expand Up @@ -558,7 +628,7 @@ def test_complete(self):
self.assertEqual(BuildItem.objects.count(), 0)

# New stock items should have been created!
self.assertEqual(StockItem.objects.count(), 13)
self.assertEqual(StockItem.objects.count(), 15)

# This stock item has been marked as "consumed"
item = StockItem.objects.get(pk=self.stock_1_1.pk)
Expand All @@ -573,6 +643,26 @@ def test_complete(self):
for output in outputs:
self.assertFalse(output.is_building)

def test_complete_with_required_tests(self):
"""Test the prevention completion when a required test is missing feature"""

# with required tests incompleted the save should fail
common.models.InvenTreeSetting.set_setting('PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', True, change_user=None)

with self.assertRaises(ValidationError):
self.build_w_tests_trackable.complete_build_output(self.stockitem_with_required_test, None)

# let's complete the required test and see if it could be saved
StockItemTestResult.objects.create(
stock_item=self.stockitem_with_required_test,
test=self.test_template_required.test_name,
result=True
)
self.build_w_tests_trackable.complete_build_output(self.stockitem_with_required_test, None)

# let's see if a non required test could be saved
self.build_wo_tests_trackable.complete_build_output(self.stockitem_wo_required_test, None)

def test_overdue_notification(self):
"""Test sending of notifications when a build order is overdue."""
self.build.target_date = datetime.now().date() - timedelta(days=1)
Expand Down
8 changes: 8 additions & 0 deletions InvenTree/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1981,6 +1981,14 @@ def save(self, *args, **kwargs):
'default': False,
'validator': bool,
},
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS': {
'name': _('Block Until Tests Pass'),
'description': _(
'Prevent build outputs from being completed until all required tests pass'
),
'default': False,
'validator': bool,
},
}

typ = 'inventree'
Expand Down
9 changes: 9 additions & 0 deletions InvenTree/common/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,12 @@ def stock_expiry_enabled():
from common.models import InvenTreeSetting

return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY', False, create=False)


def prevent_build_output_complete_on_incompleted_tests():
"""Returns True if the completion of the build outputs is disabled until the required tests are passed."""
from common.models import InvenTreeSetting

return InvenTreeSetting.get_setting(
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', False, create=False
)
1 change: 1 addition & 0 deletions InvenTree/templates/InvenTree/settings/build.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PATTERN" %}
{% include "InvenTree/settings/setting.html" with key="PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS" %}
</tbody>
</table>

Expand Down
9 changes: 8 additions & 1 deletion src/frontend/src/pages/Index/Settings/SystemSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,14 @@ export default function SystemSettings() {
name: 'buildorders',
label: t`Build Orders`,
icon: <IconTools />,
content: <GlobalSettingList keys={['BUILDORDER_REFERENCE_PATTERN']} />
content: (
<GlobalSettingList
keys={[
'BUILDORDER_REFERENCE_PATTERN',
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS'
]}
/>
)
},
{
name: 'purchaseorders',
Expand Down

0 comments on commit ad1c1ae

Please sign in to comment.