mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 11:35:41 +00:00
[WIP] Testable parts (#7888)
* Add "testable" field to the Part model - Default = False - Determines whether a particular part can have tests defined against it * Adds data migration to set default 'testable' state * Update part serializers * CUI: Update table filters * PUI: Update tables and filters * CUI: Update part detail page * PUI: Update part detail page * Update CUI * Update build pages * Update BuildLine serializer * Bump API version * Update docs * Add 'testable' to fieldset
This commit is contained in:
@ -1,13 +1,17 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 238
|
||||
INVENTREE_API_VERSION = 239
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v239 - 2024-08-15 : https://github.com/inventree/InvenTree/pull/7888
|
||||
- Adds "testable" field to the Part model
|
||||
- Adds associated filters to various API endpoints
|
||||
|
||||
v238 - 2024-08-14 : https://github.com/inventree/InvenTree/pull/7874
|
||||
- Add "assembly" filter to BuildLine API endpoint
|
||||
|
||||
|
@ -292,6 +292,7 @@ class BuildLineFilter(rest_filters.FilterSet):
|
||||
optional = rest_filters.BooleanFilter(label=_('Optional'), field_name='bom_item__optional')
|
||||
assembly = rest_filters.BooleanFilter(label=_('Assembly'), field_name='bom_item__sub_part__assembly')
|
||||
tracked = rest_filters.BooleanFilter(label=_('Tracked'), field_name='bom_item__sub_part__trackable')
|
||||
testable = rest_filters.BooleanFilter(label=_('Testable'), field_name='bom_item__sub_part__testable')
|
||||
|
||||
allocated = rest_filters.BooleanFilter(label=_('Allocated'), method='filter_allocated')
|
||||
|
||||
|
@ -1238,6 +1238,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
'reference',
|
||||
'consumable',
|
||||
'optional',
|
||||
'testable',
|
||||
'trackable',
|
||||
'inherited',
|
||||
'allow_variants',
|
||||
@ -1282,6 +1283,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
reference = serializers.CharField(source='bom_item.reference', label=_('Reference'), read_only=True)
|
||||
consumable = serializers.BooleanField(source='bom_item.consumable', label=_('Consumable'), read_only=True)
|
||||
optional = serializers.BooleanField(source='bom_item.optional', label=_('Optional'), read_only=True)
|
||||
testable = serializers.BooleanField(source='bom_item.sub_part.testable', label=_('Testable'), read_only=True)
|
||||
trackable = serializers.BooleanField(source='bom_item.sub_part.trackable', label=_('Trackable'), read_only=True)
|
||||
inherited = serializers.BooleanField(source='bom_item.inherited', label=_('Inherited'), read_only=True)
|
||||
allow_variants = serializers.BooleanField(source='bom_item.allow_variants', label=_('Allow Variants'), read_only=True)
|
||||
|
@ -332,7 +332,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
});
|
||||
});
|
||||
|
||||
{% if build.part.trackable > 0 %}
|
||||
{% if build.part.testable %}
|
||||
onPanelLoad("test-statistics", function() {
|
||||
prepareTestStatisticsTable('build', '{% url "api-test-statistics-by-build" build.pk %}')
|
||||
});
|
||||
|
@ -388,6 +388,7 @@ onPanelLoad('outputs', function() {
|
||||
source_location: {{ build.take_from.pk }},
|
||||
{% endif %}
|
||||
tracked_parts: true,
|
||||
testable: {% js_bool build.part.testable %},
|
||||
trackable: {% js_bool build.part.trackable %}
|
||||
};
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
{% include "sidebar_item.html" with label='consumed' text=text icon="fa-tasks" %}
|
||||
{% trans "Child Build Orders" as text %}
|
||||
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
|
||||
{% if build.part.trackable %}
|
||||
{% if build.part.testable %}
|
||||
{% trans "Test Statistics" as text %}
|
||||
{% include "sidebar_item.html" with label='test-statistics' text=text icon="fa-chart-line" %}
|
||||
{% endif %}
|
||||
|
@ -1157,6 +1157,8 @@ class PartFilter(rest_filters.FilterSet):
|
||||
|
||||
trackable = rest_filters.BooleanFilter()
|
||||
|
||||
testable = rest_filters.BooleanFilter()
|
||||
|
||||
purchaseable = rest_filters.BooleanFilter()
|
||||
|
||||
salable = rest_filters.BooleanFilter()
|
||||
@ -1748,20 +1750,28 @@ class BomFilter(rest_filters.FilterSet):
|
||||
|
||||
# Filters for linked 'part'
|
||||
part_active = rest_filters.BooleanFilter(
|
||||
label='Master part is active', field_name='part__active'
|
||||
label='Assembly part is active', field_name='part__active'
|
||||
)
|
||||
|
||||
part_trackable = rest_filters.BooleanFilter(
|
||||
label='Master part is trackable', field_name='part__trackable'
|
||||
label='Assembly part is trackable', field_name='part__trackable'
|
||||
)
|
||||
|
||||
part_testable = rest_filters.BooleanFilter(
|
||||
label=_('Assembly part is testable'), field_name='part__testable'
|
||||
)
|
||||
|
||||
# Filters for linked 'sub_part'
|
||||
sub_part_trackable = rest_filters.BooleanFilter(
|
||||
label='Sub part is trackable', field_name='sub_part__trackable'
|
||||
label='Component part is trackable', field_name='sub_part__trackable'
|
||||
)
|
||||
|
||||
sub_part_testable = rest_filters.BooleanFilter(
|
||||
label=_('Component part is testable'), field_name='sub_part__testable'
|
||||
)
|
||||
|
||||
sub_part_assembly = rest_filters.BooleanFilter(
|
||||
label='Sub part is an assembly', field_name='sub_part__assembly'
|
||||
label='Component part is an assembly', field_name='sub_part__assembly'
|
||||
)
|
||||
|
||||
available_stock = rest_filters.BooleanFilter(
|
||||
|
18
src/backend/InvenTree/part/migrations/0128_part_testable.py
Normal file
18
src/backend/InvenTree/part/migrations/0128_part_testable.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-15 02:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0127_remove_partcategory_icon_partcategory__icon'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='part',
|
||||
name='testable',
|
||||
field=models.BooleanField(default=False, help_text='Can this part have test results recorded against it?', verbose_name='Testable'),
|
||||
),
|
||||
]
|
@ -0,0 +1,37 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-15 02:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_testable(apps, schema_editor):
|
||||
"""Set the 'testable' status to True for certain parts.
|
||||
|
||||
Prior to migration part.0128, the 'trackable' attribute
|
||||
was used to determine if parts could have tests associated with them.
|
||||
|
||||
However, 'trackable' comes with other restrictions
|
||||
(such as requiring a unique serial number).
|
||||
|
||||
So, we have added a new field 'testable' to the Part model,
|
||||
which is updated in this migration to match the value of the 'trackable' field.
|
||||
"""
|
||||
|
||||
Part = apps.get_model('part', 'Part')
|
||||
|
||||
# By default, 'testable' is False - so we only need to update parts marked as 'trackable'
|
||||
trackable_parts = Part.objects.filter(trackable=True)
|
||||
|
||||
if trackable_parts.count() > 0:
|
||||
print(f"\nMarking {trackable_parts.count()} Part objects as 'testable'")
|
||||
trackable_parts.update(testable=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0128_part_testable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_testable, reverse_code=migrations.RunPython.noop)
|
||||
]
|
@ -403,6 +403,7 @@ class Part(
|
||||
component: Can this part be used to make other parts?
|
||||
purchaseable: Can this part be purchased from suppliers?
|
||||
trackable: Trackable parts can have unique serial numbers assigned, etc, etc
|
||||
testable: Testable parts can have test results recorded against their stock items
|
||||
active: Is this part active? Parts are deactivated instead of being deleted
|
||||
locked: This part is locked and cannot be edited
|
||||
virtual: Is this part "virtual"? e.g. a software product or similar
|
||||
@ -1166,6 +1167,12 @@ class Part(
|
||||
help_text=_('Does this part have tracking for unique items?'),
|
||||
)
|
||||
|
||||
testable = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Testable'),
|
||||
help_text=_('Can this part have test results recorded against it?'),
|
||||
)
|
||||
|
||||
purchaseable = models.BooleanField(
|
||||
default=part_settings.part_purchaseable_default,
|
||||
verbose_name=_('Purchaseable'),
|
||||
|
@ -323,6 +323,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'is_template',
|
||||
'purchaseable',
|
||||
'salable',
|
||||
'testable',
|
||||
'trackable',
|
||||
'virtual',
|
||||
'units',
|
||||
@ -673,6 +674,7 @@ class PartSerializer(
|
||||
'salable',
|
||||
'starred',
|
||||
'thumbnail',
|
||||
'testable',
|
||||
'trackable',
|
||||
'units',
|
||||
'variant_of',
|
||||
|
@ -50,7 +50,7 @@
|
||||
{% trans "Stocktake" as text %}
|
||||
{% include "sidebar_item.html" with label="stocktake" text=text icon="fa-clipboard-check" %}
|
||||
{% endif %}
|
||||
{% if part.trackable %}
|
||||
{% if part.testable %}
|
||||
{% trans "Test Templates" as text %}
|
||||
{% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %}
|
||||
{% trans "Test Statistics" as text %}
|
||||
|
@ -8,7 +8,7 @@
|
||||
{% trans "Allocations" as text %}
|
||||
{% include "sidebar_item.html" with label="allocations" text=text icon="fa-bookmark" %}
|
||||
{% endif %}
|
||||
{% if item.part.trackable %}
|
||||
{% if item.part.testable %}
|
||||
{% trans "Test Data" as text %}
|
||||
{% include "sidebar_item.html" with label='test-data' text=text icon="fa-vial" %}
|
||||
{% endif %}
|
||||
|
@ -1297,7 +1297,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
});
|
||||
|
||||
// Request list of required tests for the part being assembled
|
||||
if (build_info.trackable) {
|
||||
if (build_info.testable) {
|
||||
inventreeGet(
|
||||
'{% url "api-part-test-template-list" %}',
|
||||
{
|
||||
|
@ -188,6 +188,9 @@ function partFields(options={}) {
|
||||
default: global_settings.PART_TEMPLATE,
|
||||
group: 'attributes',
|
||||
},
|
||||
testable: {
|
||||
group: 'attributes',
|
||||
},
|
||||
trackable: {
|
||||
default: global_settings.PART_TRACKABLE,
|
||||
group: 'attributes',
|
||||
|
@ -142,6 +142,10 @@ function getVariantsTableFilters() {
|
||||
type: 'bool',
|
||||
title: '{% trans "Virtual" %}',
|
||||
},
|
||||
testable: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Testable" %}',
|
||||
},
|
||||
trackable: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Trackable" %}',
|
||||
@ -153,6 +157,10 @@ function getVariantsTableFilters() {
|
||||
// Return a dictionary of filters for the BOM table
|
||||
function getBOMTableFilters() {
|
||||
return {
|
||||
sub_part_testable: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Testable Part" %}',
|
||||
},
|
||||
sub_part_trackable: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Trackable Part" %}',
|
||||
@ -785,6 +793,10 @@ function getPartTableFilters() {
|
||||
type: 'bool',
|
||||
title: '{% trans "Template" %}',
|
||||
},
|
||||
testable: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Testable" %}',
|
||||
},
|
||||
trackable: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Trackable" %}',
|
||||
|
@ -55,6 +55,7 @@ export function usePartFields({
|
||||
component: {},
|
||||
assembly: {},
|
||||
is_template: {},
|
||||
testable: {},
|
||||
trackable: {},
|
||||
purchaseable: {},
|
||||
salable: {},
|
||||
|
@ -129,6 +129,7 @@ const icons = {
|
||||
shipment: IconTruckDelivery,
|
||||
scheduling: IconCalendarStats,
|
||||
test_templates: IconTestPipe,
|
||||
test: IconTestPipe,
|
||||
related_parts: IconLayersLinked,
|
||||
attachments: IconPaperclip,
|
||||
note: IconNotes,
|
||||
|
@ -322,7 +322,7 @@ export default function BuildDetail() {
|
||||
name: 'test-results',
|
||||
label: t`Test Results`,
|
||||
icon: <IconChecklist />,
|
||||
hidden: !build.part_detail?.trackable,
|
||||
hidden: !build.part_detail?.testable,
|
||||
content: build.pk ? (
|
||||
<BuildOrderTestTable buildId={build.pk} partId={build.part} />
|
||||
) : (
|
||||
@ -341,7 +341,7 @@ export default function BuildDetail() {
|
||||
}}
|
||||
/>
|
||||
),
|
||||
hidden: !build?.part_detail?.trackable
|
||||
hidden: !build?.part_detail?.testable
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
|
@ -315,6 +315,12 @@ export default function PartDetail() {
|
||||
name: 'component',
|
||||
label: t`Component Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'testable',
|
||||
label: t`Testable Part`,
|
||||
icon: 'test'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'trackable',
|
||||
@ -674,7 +680,7 @@ export default function PartDetail() {
|
||||
name: 'test_templates',
|
||||
label: t`Test Templates`,
|
||||
icon: <IconTestPipe />,
|
||||
hidden: !part.trackable,
|
||||
hidden: !part.testable,
|
||||
content: part?.pk ? (
|
||||
<PartTestTemplateTable partId={part?.pk} partLocked={part.locked} />
|
||||
) : (
|
||||
@ -685,7 +691,7 @@ export default function PartDetail() {
|
||||
name: 'test_statistics',
|
||||
label: t`Test Statistics`,
|
||||
icon: <IconReportAnalytics />,
|
||||
hidden: !part.trackable,
|
||||
hidden: !part.testable,
|
||||
content: part?.pk ? (
|
||||
<TestStatisticsTable
|
||||
params={{
|
||||
|
@ -119,7 +119,7 @@ export default function StockDetail() {
|
||||
name: 'tests',
|
||||
label: `Completed Tests`,
|
||||
icon: 'progress',
|
||||
hidden: !part?.trackable
|
||||
hidden: !part?.testable
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
@ -348,7 +348,7 @@ export default function StockDetail() {
|
||||
name: 'testdata',
|
||||
label: t`Test Data`,
|
||||
icon: <IconChecklist />,
|
||||
hidden: !stockitem?.part_detail?.trackable,
|
||||
hidden: !stockitem?.part_detail?.testable,
|
||||
content: stockitem?.pk ? (
|
||||
<StockItemTestResultTable
|
||||
itemId={stockitem.pk}
|
||||
|
@ -299,6 +299,11 @@ export function BomTable({
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'sub_part_testable',
|
||||
label: t`Testable Part`,
|
||||
description: t`Show testable items`
|
||||
},
|
||||
{
|
||||
name: 'sub_part_trackable',
|
||||
label: t`Trackable Part`,
|
||||
|
@ -61,6 +61,11 @@ export default function BuildLineTable({
|
||||
label: t`Assembly`,
|
||||
description: t`Show assembled items`
|
||||
},
|
||||
{
|
||||
name: 'testable',
|
||||
label: t`Testable`,
|
||||
description: t`Show testable items`
|
||||
},
|
||||
{
|
||||
name: 'tracked',
|
||||
label: t`Tracked`,
|
||||
|
@ -47,12 +47,17 @@ export default function BuildOutputTable({ build }: { build: any }) {
|
||||
|
||||
// Fetch the test templates associated with the partId
|
||||
const { data: testTemplates } = useQuery({
|
||||
queryKey: ['buildoutputtests', partId],
|
||||
queryKey: ['buildoutputtests', partId, build],
|
||||
queryFn: async () => {
|
||||
if (!partId || partId < 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If the part is not testable, return an empty array
|
||||
if (!build?.part_detail?.testable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return api
|
||||
.get(apiUrl(ApiEndpoints.part_test_template_list), {
|
||||
params: {
|
||||
|
@ -203,6 +203,12 @@ function partTableFilters(): TableFilter[] {
|
||||
description: t`Filter by component attribute`,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'testable',
|
||||
label: t`Testable`,
|
||||
description: t`Filter by testable attribute`,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'trackable',
|
||||
label: t`Trackable`,
|
||||
|
Reference in New Issue
Block a user