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

Relation update (#8524)

* Add "note" field to PartRelated model

* Improved API

* Add field to serializer

* Implement in RelatedPartTable

* Bump API version
This commit is contained in:
Oliver 2024-11-20 08:54:19 +11:00 committed by GitHub
parent 8f1a3a1ab7
commit 7fcf068f05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 26 deletions

View File

@ -1,13 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 282
INVENTREE_API_VERSION = 283
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v283 - 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
- Adds "note" field to the PartRelated API endpoint
v282 - 2024-11-19 : https://github.com/inventree/InvenTree/pull/8487
- Remove the "test statistics" API endpoints
- This is now provided via a custom plugin

View File

@ -1427,37 +1427,50 @@ class PartDetail(PartMixin, RetrieveUpdateDestroyAPI):
return response
class PartRelatedList(ListCreateAPI):
"""API endpoint for accessing a list of PartRelated objects."""
class PartRelatedFilter(rest_filters.FilterSet):
"""FilterSet for PartRelated objects."""
class Meta:
"""Metaclass options."""
model = PartRelated
fields = ['part_1', 'part_2']
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(), method='filter_part', label=_('Part')
)
def filter_part(self, queryset, name, part):
"""Filter queryset to include only PartRelated objects which reference the specified part."""
return queryset.filter(Q(part_1=part) | Q(part_2=part)).distinct()
class PartRelatedMixin:
"""Mixin class for PartRelated API endpoints."""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
def filter_queryset(self, queryset):
"""Custom queryset filtering."""
queryset = super().filter_queryset(queryset)
def get_queryset(self, *args, **kwargs):
"""Return an annotated queryset for the PartRelatedDetail endpoint."""
queryset = super().get_queryset(*args, **kwargs)
params = self.request.query_params
# Add a filter for "part" - we can filter either part_1 or part_2
part = params.get('part', None)
if part is not None:
try:
part = Part.objects.get(pk=part)
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part)).distinct()
except (ValueError, Part.DoesNotExist):
pass
queryset = queryset.prefetch_related('part_1', 'part_2')
return queryset
class PartRelatedDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for accessing detail view of a PartRelated object."""
class PartRelatedList(PartRelatedMixin, ListCreateAPI):
"""API endpoint for accessing a list of PartRelated objects."""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
filterset_class = PartRelatedFilter
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['part_1__name', 'part_2__name']
class PartRelatedDetail(PartRelatedMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for accessing detail view of a PartRelated object."""
class PartParameterTemplateFilter(rest_filters.FilterSet):

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-11-19 12:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0130_alter_parttesttemplate_part'),
]
operations = [
migrations.AddField(
model_name='partrelated',
name='note',
field=models.CharField(blank=True, help_text='Note for this relationship', max_length=500, verbose_name='Note'),
),
]

View File

@ -4680,6 +4680,13 @@ class PartRelated(InvenTree.models.InvenTreeMetadataModel):
help_text=_('Select Related Part'),
)
note = models.CharField(
max_length=500,
blank=True,
verbose_name=_('Note'),
help_text=_('Note for this relationship'),
)
def __str__(self):
"""Return a string representation of this Part-Part relationship."""
return f'{self.part_1} <--> {self.part_2}'

View File

@ -1505,7 +1505,7 @@ class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Metaclass defining serializer fields."""
model = PartRelated
fields = ['pk', 'part_1', 'part_1_detail', 'part_2', 'part_2_detail']
fields = ['pk', 'part_1', 'part_1_detail', 'part_2', 'part_2_detail', 'note']
part_1_detail = PartSerializer(source='part_1', read_only=True, many=False)
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False)

View File

@ -10,14 +10,15 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import {
useCreateApiFormModal,
useDeleteApiFormModal
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import type { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { type RowAction, RowDeleteAction } from '../RowActions';
import { type RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
/**
* Construct a table listing related parts for a given part
@ -45,6 +46,7 @@ export function RelatedPartTable({
{
accessor: 'part',
title: t`Part`,
switchable: false,
render: (record: any) => {
const part = getPart(record);
return (
@ -63,11 +65,16 @@ export function RelatedPartTable({
},
{
accessor: 'description',
title: t`Description`,
title: t`Part Description`,
ellipsis: true,
render: (record: any) => {
return getPart(record).description;
}
},
{
accessor: 'note',
title: t`Note`,
sortable: false
}
];
}, [partId]);
@ -102,6 +109,16 @@ export function RelatedPartTable({
table: table
});
const editRelatedPart = useEditApiFormModal({
url: ApiEndpoints.related_part_list,
pk: selectedRelatedPart,
title: t`Edit Related Part`,
fields: {
note: {}
},
table: table
});
const tableActions: ReactNode[] = useMemo(() => {
return [
<AddItemButton
@ -116,6 +133,13 @@ export function RelatedPartTable({
const rowActions = useCallback(
(record: any): RowAction[] => {
return [
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => {
setSelectedRelatedPart(record.pk);
editRelatedPart.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => {
@ -131,6 +155,7 @@ export function RelatedPartTable({
return (
<>
{newRelatedPart.modal}
{editRelatedPart.modal}
{deleteRelatedPart.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.related_part_list)}