From 9ceaad08482a768d578a27abbf9424010090b61a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 24 Nov 2025 11:28:00 +0000 Subject: [PATCH] Remove code for ManufacturerPartParameter --- .../migrations/0041_auto_20251028_1112.py | 98 +++++++++++- src/backend/InvenTree/common/serializers.py | 1 + src/backend/InvenTree/company/admin.py | 12 -- src/backend/InvenTree/company/api.py | 68 --------- src/backend/InvenTree/company/models.py | 2 + src/backend/InvenTree/company/serializers.py | 28 ---- .../pages/company/ManufacturerPartDetail.tsx | 19 +-- .../src/pages/company/SupplierPartDetail.tsx | 5 + .../ManufacturerPartParameterTable.tsx | 140 ------------------ 9 files changed, 108 insertions(+), 265 deletions(-) delete mode 100644 src/frontend/src/tables/purchasing/ManufacturerPartParameterTable.tsx diff --git a/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py b/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py index 1b3128564d..36dd756385 100644 --- a/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py +++ b/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py @@ -2,6 +2,53 @@ from django.db import migrations +def convert_to_numeric_value(value: str, units: str): + """Convert a value (with units) to a numeric value. + + Defaults to zero if the value cannot be converted. + """ + + import InvenTree.conversion + + # Default value is null + result = None + + if units: + try: + result = InvenTree.conversion.convert_physical_value(value, units) + result = float(result.magnitude) + except Exception: + pass + else: + try: + result = float(value) + except Exception: + pass + + return result + + +def remove_existing_parameters(apps, schema_editor): + """Remove any existing Parameter or ParameterTemplate objects from the database.""" + + Parameter = apps.get_model("common", "Parameter") + ParameterTemplate = apps.get_model("common", "ParameterTemplate") + + n_params = Parameter.objects.count() + n_templates = ParameterTemplate.objects.count() + + Parameter.objects.all().delete() + ParameterTemplate.objects.all().delete() + + if n_params > 0: + print(f"Removed {n_params} existing Parameter instances.") + + if n_templates > 0: + print(f"Removed {n_templates} existing ParameterTemplate instances.") + + assert Parameter.objects.count() == 0 + assert ParameterTemplate.objects.count() == 0 + def copy_part_parameters(apps, schema_editor): """Forward migration: copy from PartParameterTemplate to ParameterTemplate.""" @@ -29,14 +76,14 @@ def copy_part_parameters(apps, schema_editor): ParameterTemplate.objects.bulk_create(templates) print(f"\nMigrated {len(templates)} PartParameterTemplate instances.") - assert ParameterTemplate.objects.filter(model_type='part').count() == len(templates) + assert ParameterTemplate.objects.filter().count() == len(templates) # Next, copy PartParameter instances to Parameter instances parameters = [] for parameter in PartParameter.objects.all(): # Find the corresponding ParameterTemplate - template = ParameterTemplate.objects.get(name=parameter.template.name, model_type='part') + template = ParameterTemplate.objects.get(name=parameter.template.name) parameters.append(Parameter( template=template, @@ -56,6 +103,44 @@ def copy_part_parameters(apps, schema_editor): assert Parameter.objects.filter(model_type='part').count() == len(parameters) +def copy_manufacturer_part_parameters(apps, schema_editor): + """Copy ManufacturerPartParameter to Parameter.""" + + ManufacturerPartParameter = apps.get_model("company", "ManufacturerPartParameter") + Parameter = apps.get_model("common", "Parameter") + ParameterTemplate = apps.get_model("common", "ParameterTemplate") + + parameters = [] + + for parameter in ManufacturerPartParameter.objects.all(): + # Find the corresponding ParameterTemplate + template = ParameterTemplate.objects.filter(name=parameter.template.name).first() + + if not template: + # A matching template does not exist - let's create one + template = ParameterTemplate.objects.create( + name=parameter.name, + description='', + units=parameter.units, + checkbox=False + ) + + parameters.append(Parameter( + template=template, + model_type='manufacturerpart', + model_id=parameter.manufacturer_part.id, + data=parameter.value, + data_numeric=convert_to_numeric_value(parameter.value), + note=parameter.note + )) + + if len(parameters) > 0: + Parameter.objects.bulk_create(parameters) + print(f"\nMigrated {len(parameters)} ManufacturerPartParameter instances.") + + assert Parameter.objects.filter(model_type='manufacturerpart').count() == len(parameters) + + class Migration(migrations.Migration): dependencies = [ @@ -64,10 +149,17 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython( + remove_existing_parameters, + reverse_code=migrations.RunPython.noop + ), migrations.RunPython( copy_part_parameters, reverse_code=migrations.RunPython.noop ), - # TODO: Data migration for existing ManufacturerPartParameter objects + migrations.RunPython( + copy_manufacturer_part_parameters, + reverse_code=migrations.RunPython.noop + ) # TODO: Data migration for existing CategoryParameter objects ] diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py index 878484e6e7..fd4b7b6f83 100644 --- a/src/backend/InvenTree/common/serializers.py +++ b/src/backend/InvenTree/common/serializers.py @@ -740,6 +740,7 @@ class ParameterTemplateSerializer( ) +@register_importer() class ParameterSerializer( FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer ): diff --git a/src/backend/InvenTree/company/admin.py b/src/backend/InvenTree/company/admin.py index 64610709af..b35218ad1e 100644 --- a/src/backend/InvenTree/company/admin.py +++ b/src/backend/InvenTree/company/admin.py @@ -9,7 +9,6 @@ from .models import ( Company, Contact, ManufacturerPart, - ManufacturerPartParameter, SupplierPart, SupplierPriceBreak, ) @@ -56,17 +55,6 @@ class ManufacturerPartAdmin(admin.ModelAdmin): autocomplete_fields = ('part', 'manufacturer') -@admin.register(ManufacturerPartParameter) -class ManufacturerPartParameterAdmin(admin.ModelAdmin): - """Admin class for ManufacturerPartParameter model.""" - - list_display = ('manufacturer_part', 'name', 'value') - - search_fields = ['manufacturer_part__manufacturer__name', 'name', 'value'] - - autocomplete_fields = ('manufacturer_part',) - - @admin.register(SupplierPriceBreak) class SupplierPriceBreakAdmin(admin.ModelAdmin): """Admin class for the SupplierPriceBreak model.""" diff --git a/src/backend/InvenTree/company/api.py b/src/backend/InvenTree/company/api.py index cde689119e..f45bff2c14 100644 --- a/src/backend/InvenTree/company/api.py +++ b/src/backend/InvenTree/company/api.py @@ -24,7 +24,6 @@ from .models import ( Company, Contact, ManufacturerPart, - ManufacturerPartParameter, SupplierPart, SupplierPriceBreak, ) @@ -32,7 +31,6 @@ from .serializers import ( AddressSerializer, CompanySerializer, ContactSerializer, - ManufacturerPartParameterSerializer, ManufacturerPartSerializer, SupplierPartSerializer, SupplierPriceBreakSerializer, @@ -217,56 +215,6 @@ class ManufacturerPartDetail(RetrieveUpdateDestroyAPI): serializer_class = ManufacturerPartSerializer -class ManufacturerPartParameterFilter(FilterSet): - """Custom filterset for the ManufacturerPartParameterList API endpoint.""" - - class Meta: - """Metaclass options.""" - - model = ManufacturerPartParameter - fields = ['name', 'value', 'units', 'manufacturer_part'] - - manufacturer = rest_filters.ModelChoiceFilter( - queryset=Company.objects.all(), field_name='manufacturer_part__manufacturer' - ) - - part = rest_filters.ModelChoiceFilter( - queryset=part.models.Part.objects.all(), field_name='manufacturer_part__part' - ) - - -class ManufacturerPartParameterOptions(OutputConfiguration): - """Available output options for the ManufacturerPartParameter endpoints.""" - - OPTIONS = [ - InvenTreeOutputOption( - description='Include detailed information about the linked ManufacturerPart in the response', - flag='manufacturer_part_detail', - default=False, - ) - ] - - -class ManufacturerPartParameterList( - SerializerContextMixin, ListCreateDestroyAPIView, OutputOptionsMixin -): - """API endpoint for list view of ManufacturerPartParamater model.""" - - queryset = ManufacturerPartParameter.objects.all() - serializer_class = ManufacturerPartParameterSerializer - filterset_class = ManufacturerPartParameterFilter - output_options = ManufacturerPartParameterOptions - filter_backends = SEARCH_ORDER_FILTER - search_fields = ['name', 'value', 'units'] - - -class ManufacturerPartParameterDetail(RetrieveUpdateDestroyAPI): - """API endpoint for detail view of ManufacturerPartParameter model.""" - - queryset = ManufacturerPartParameter.objects.all() - serializer_class = ManufacturerPartParameterSerializer - - class SupplierPartFilter(FilterSet): """API filters for the SupplierPartList endpoint.""" @@ -518,22 +466,6 @@ class SupplierPriceBreakDetail(SupplierPriceBreakMixin, RetrieveUpdateDestroyAPI manufacturer_part_api_urls = [ - path( - 'parameter/', - include([ - path( - '/', - ManufacturerPartParameterDetail.as_view(), - name='api-manufacturer-part-parameter-detail', - ), - # Catch anything else - path( - '', - ManufacturerPartParameterList.as_view(), - name='api-manufacturer-part-parameter-list', - ), - ]), - ), path( '/', include([ diff --git a/src/backend/InvenTree/company/models.py b/src/backend/InvenTree/company/models.py index b4bf796565..f642e244d2 100644 --- a/src/backend/InvenTree/company/models.py +++ b/src/backend/InvenTree/company/models.py @@ -473,6 +473,7 @@ class Address(InvenTree.models.InvenTreeModel): class ManufacturerPart( InvenTree.models.InvenTreeAttachmentMixin, + InvenTree.models.InvenTreeParameterMixin, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNotesMixin, InvenTree.models.InvenTreeMetadataModel, @@ -652,6 +653,7 @@ class SupplierPartManager(models.Manager): class SupplierPart( InvenTree.models.InvenTreeAttachmentMixin, + InvenTree.models.InvenTreeParameterMixin, InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNotesMixin, diff --git a/src/backend/InvenTree/company/serializers.py b/src/backend/InvenTree/company/serializers.py index 5488abbf43..f6ce2ff3f3 100644 --- a/src/backend/InvenTree/company/serializers.py +++ b/src/backend/InvenTree/company/serializers.py @@ -36,7 +36,6 @@ from .models import ( Company, Contact, ManufacturerPart, - ManufacturerPartParameter, SupplierPart, SupplierPriceBreak, ) @@ -302,33 +301,6 @@ class ManufacturerPartSerializer( ) -@register_importer() -class ManufacturerPartParameterSerializer( - FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer -): - """Serializer for the ManufacturerPartParameter model.""" - - class Meta: - """Metaclass options.""" - - model = ManufacturerPartParameter - - fields = [ - 'pk', - 'manufacturer_part', - 'manufacturer_part_detail', - 'name', - 'value', - 'units', - ] - - manufacturer_part_detail = enable_filter( - ManufacturerPartSerializer( - source='manufacturer_part', many=False, read_only=True, allow_null=True - ) - ) - - class SupplierPriceBreakBriefSerializer( FilterableSerializerMixin, InvenTreeModelSerializer ): diff --git a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx index 693906af1a..b9d0fc2cee 100644 --- a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx +++ b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx @@ -3,7 +3,6 @@ import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconBuildingWarehouse, IconInfoCircle, - IconList, IconPackages } from '@tabler/icons-react'; import { useMemo } from 'react'; @@ -33,6 +32,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel'; import NotesPanel from '../../components/panels/NotesPanel'; import type { PanelType } from '../../components/panels/Panel'; import { PanelGroup } from '../../components/panels/PanelGroup'; +import ParametersPanel from '../../components/panels/ParametersPanel'; import { useManufacturerPartFields } from '../../forms/CompanyForms'; import { useCreateApiFormModal, @@ -41,7 +41,6 @@ import { } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; import { useUserState } from '../../states/UserState'; -import ManufacturerPartParameterTable from '../../tables/purchasing/ManufacturerPartParameterTable'; import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; @@ -161,18 +160,10 @@ export default function ManufacturerPartDetail() { icon: , content: detailsPanel }, - { - name: 'parameters', - label: t`Parameters`, - icon: , - content: manufacturerPart?.pk ? ( - - ) : ( - - ) - }, + ParametersPanel({ + model_type: ModelType.manufacturerpart, + model_id: manufacturerPart?.pk + }), { name: 'stock', label: t`Received Stock`, diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx index 35cdf4f836..9714180a24 100644 --- a/src/frontend/src/pages/company/SupplierPartDetail.tsx +++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx @@ -36,6 +36,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel'; import NotesPanel from '../../components/panels/NotesPanel'; import type { PanelType } from '../../components/panels/Panel'; import { PanelGroup } from '../../components/panels/PanelGroup'; +import ParametersPanel from '../../components/panels/ParametersPanel'; import { useSupplierPartFields } from '../../forms/CompanyForms'; import { useCreateApiFormModal, @@ -247,6 +248,10 @@ export default function SupplierPartDetail() { icon: , content: detailsPanel }, + ParametersPanel({ + model_type: ModelType.supplierpart, + model_id: supplierPart?.pk + }), { name: 'stock', label: t`Received Stock`, diff --git a/src/frontend/src/tables/purchasing/ManufacturerPartParameterTable.tsx b/src/frontend/src/tables/purchasing/ManufacturerPartParameterTable.tsx deleted file mode 100644 index 6341e12e50..0000000000 --- a/src/frontend/src/tables/purchasing/ManufacturerPartParameterTable.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { t } from '@lingui/core/macro'; -import { useCallback, useMemo, useState } from 'react'; - -import { AddItemButton } from '@lib/components/AddItemButton'; -import { - type RowAction, - RowDeleteAction, - RowEditAction -} from '@lib/components/RowActions'; -import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; -import { UserRoles } from '@lib/enums/Roles'; -import { apiUrl } from '@lib/functions/Api'; -import type { TableColumn } from '@lib/types/Tables'; -import { useManufacturerPartParameterFields } from '../../forms/CompanyForms'; -import { - useCreateApiFormModal, - useDeleteApiFormModal, - useEditApiFormModal -} from '../../hooks/UseForm'; -import { useTable } from '../../hooks/UseTable'; -import { useUserState } from '../../states/UserState'; -import { InvenTreeTable } from '../InvenTreeTable'; - -export default function ManufacturerPartParameterTable({ - params -}: Readonly<{ - params: any; -}>) { - const table = useTable('manufacturer-part-parameter'); - const user = useUserState(); - - const tableColumns: TableColumn[] = useMemo(() => { - return [ - { - accessor: 'name', - title: t`Name`, - sortable: true, - switchable: false - }, - { - accessor: 'value', - title: t`Value`, - sortable: true, - switchable: false - }, - { - accessor: 'units', - title: t`Units`, - sortable: false, - switchable: true - } - ]; - }, []); - - const fields = useManufacturerPartParameterFields(); - - const [selectedParameter, setSelectedParameter] = useState< - number | undefined - >(undefined); - - const createParameter = useCreateApiFormModal({ - url: ApiEndpoints.manufacturer_part_parameter_list, - title: t`Add Parameter`, - fields: fields, - table: table, - initialData: { - manufacturer_part: params.manufacturer_part - } - }); - - const editParameter = useEditApiFormModal({ - url: ApiEndpoints.manufacturer_part_parameter_list, - pk: selectedParameter, - title: t`Edit Parameter`, - fields: fields, - table: table - }); - - const deleteParameter = useDeleteApiFormModal({ - url: ApiEndpoints.manufacturer_part_parameter_list, - pk: selectedParameter, - title: t`Delete Parameter`, - table: table - }); - - const rowActions = useCallback( - (record: any): RowAction[] => { - return [ - RowEditAction({ - hidden: !user.hasChangeRole(UserRoles.purchase_order), - onClick: () => { - setSelectedParameter(record.pk); - editParameter.open(); - } - }), - RowDeleteAction({ - hidden: !user.hasDeleteRole(UserRoles.purchase_order), - onClick: () => { - setSelectedParameter(record.pk); - deleteParameter.open(); - } - }) - ]; - }, - [user] - ); - - const tableActions = useMemo(() => { - return [ - { - createParameter.open(); - }} - hidden={!user.hasAddRole(UserRoles.purchase_order)} - /> - ]; - }, [user]); - - return ( - <> - {createParameter.modal} - {editParameter.modal} - {deleteParameter.modal} - - - ); -}