mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
7adf2e0835
commit
ad1c1ae604
InvenTree
build
common
templates/InvenTree/settings
src/frontend/src/pages/Index/Settings
@ -915,6 +915,11 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
|
||||
# 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)
|
||||
|
@ -27,6 +27,7 @@ from InvenTree.status_codes import BuildStatusGroups, StockStatus
|
||||
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
|
||||
@ -523,6 +524,17 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
|
||||
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"))
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""Unit tests for the 'build' models"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
@ -14,8 +14,8 @@ from InvenTree import status_codes as status
|
||||
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
|
||||
@ -55,6 +55,76 @@ class BuildTestBase(TestCase):
|
||||
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",
|
||||
@ -245,7 +315,7 @@ class BuildTest(BuildTestBase):
|
||||
|
||||
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)
|
||||
@ -558,7 +628,7 @@ class BuildTest(BuildTestBase):
|
||||
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)
|
||||
@ -573,6 +643,26 @@ class BuildTest(BuildTestBase):
|
||||
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)
|
||||
|
@ -1981,6 +1981,14 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'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'
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user