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:
parent
8f1a3a1ab7
commit
7fcf068f05
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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}'
|
||||
|
@ -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)
|
||||
|
@ -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)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user