2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

Add option to disable the build output completion if are tests not passed (#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 <oliver.henry.walters@gmail.com>
This commit is contained in:
Miklós Márton 2024-02-18 00:28:37 +01:00 committed by GitHub
parent 7adf2e0835
commit ad1c1ae604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 138 additions and 6 deletions

View File

@ -915,6 +915,11 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
# List the allocated BuildItem objects for the given output # List the allocated BuildItem objects for the given output
allocated_items = output.items_to_install.all() 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: for build_item in allocated_items:
# Complete the allocation of stock for that item # Complete the allocation of stock for that item
build_item.complete_allocation(user) build_item.complete_allocation(user)

View File

@ -27,6 +27,7 @@ from InvenTree.status_codes import BuildStatusGroups, StockStatus
from stock.models import generate_batch_code, StockItem, StockLocation from stock.models import generate_batch_code, StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer from stock.serializers import StockItemSerializerBrief, LocationSerializer
import common.models
from common.serializers import ProjectCodeSerializer from common.serializers import ProjectCodeSerializer
import part.filters import part.filters
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
@ -523,6 +524,17 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
outputs = data.get('outputs', []) 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: if len(outputs) == 0:
raise ValidationError(_("A list of build outputs must be provided")) raise ValidationError(_("A list of build outputs must be provided"))

View File

@ -1,5 +1,5 @@
"""Unit tests for the 'build' models""" """Unit tests for the 'build' models"""
import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.test import TestCase from django.test import TestCase
@ -14,8 +14,8 @@ from InvenTree import status_codes as status
import common.models import common.models
import build.tasks import build.tasks
from build.models import Build, BuildItem, BuildLine, generate_next_build_reference from build.models import Build, BuildItem, BuildLine, generate_next_build_reference
from part.models import Part, BomItem, BomItemSubstitute from part.models import Part, BomItem, BomItemSubstitute, PartTestTemplate
from stock.models import StockItem from stock.models import StockItem, StockItemTestResult
from users.models import Owner from users.models import Owner
import logging import logging
@ -55,6 +55,76 @@ class BuildTestBase(TestCase):
trackable=True, 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( cls.sub_part_1 = Part.objects.create(
name="Widget A", name="Widget A",
description="A widget", description="A widget",
@ -245,7 +315,7 @@ class BuildTest(BuildTestBase):
def test_init(self): def test_init(self):
"""Perform some basic tests before we start the ball rolling""" """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 # Build is PENDING
self.assertEqual(self.build.status, status.BuildStatus.PENDING) self.assertEqual(self.build.status, status.BuildStatus.PENDING)
@ -558,7 +628,7 @@ class BuildTest(BuildTestBase):
self.assertEqual(BuildItem.objects.count(), 0) self.assertEqual(BuildItem.objects.count(), 0)
# New stock items should have been created! # 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" # This stock item has been marked as "consumed"
item = StockItem.objects.get(pk=self.stock_1_1.pk) item = StockItem.objects.get(pk=self.stock_1_1.pk)
@ -573,6 +643,26 @@ class BuildTest(BuildTestBase):
for output in outputs: for output in outputs:
self.assertFalse(output.is_building) 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): def test_overdue_notification(self):
"""Test sending of notifications when a build order is overdue.""" """Test sending of notifications when a build order is overdue."""
self.build.target_date = datetime.now().date() - timedelta(days=1) self.build.target_date = datetime.now().date() - timedelta(days=1)

View File

@ -1981,6 +1981,14 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': False, 'default': False,
'validator': bool, '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' typ = 'inventree'

View File

@ -56,3 +56,12 @@ def stock_expiry_enabled():
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY', False, create=False) 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
)

View File

@ -13,6 +13,7 @@
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tbody> <tbody>
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PATTERN" %} {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PATTERN" %}
{% include "InvenTree/settings/setting.html" with key="PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS" %}
</tbody> </tbody>
</table> </table>

View File

@ -225,7 +225,14 @@ export default function SystemSettings() {
name: 'buildorders', name: 'buildorders',
label: t`Build Orders`, label: t`Build Orders`,
icon: <IconTools />, icon: <IconTools />,
content: <GlobalSettingList keys={['BUILDORDER_REFERENCE_PATTERN']} /> content: (
<GlobalSettingList
keys={[
'BUILDORDER_REFERENCE_PATTERN',
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS'
]}
/>
)
}, },
{ {
name: 'purchaseorders', name: 'purchaseorders',