diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 91497f5ec7..0079fb39be 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 224 +INVENTREE_API_VERSION = 225 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671 + - Adds "filters" field to DataImportSession API + v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667 - Add notes field to ManufacturerPart and SupplierPart API endpoints diff --git a/src/backend/InvenTree/importer/migrations/0003_dataimportsession_field_filters.py b/src/backend/InvenTree/importer/migrations/0003_dataimportsession_field_filters.py new file mode 100644 index 0000000000..b5663a5e31 --- /dev/null +++ b/src/backend/InvenTree/importer/migrations/0003_dataimportsession_field_filters.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.14 on 2024-07-16 03:04 + +from django.db import migrations, models +import importer.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('importer', '0002_dataimportsession_field_overrides'), + ] + + operations = [ + migrations.AddField( + model_name='dataimportsession', + name='field_filters', + field=models.JSONField(blank=True, null=True, validators=[importer.validators.validate_field_defaults], verbose_name='Field Filters'), + ), + ] diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py index 83c417f782..219e1ac600 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -32,8 +32,9 @@ class DataImportSession(models.Model): data_file: FileField for the data file to import status: IntegerField for the status of the import session user: ForeignKey to the User who initiated the import - field_defaults: JSONField for field default values - field_overrides: JSONField for field override values + field_defaults: JSONField for field default values - provides a backup value for a field + field_overrides: JSONField for field override values - used to force a value for a field + field_filters: JSONField for field filter values - optional field API filters """ @staticmethod @@ -101,6 +102,13 @@ class DataImportSession(models.Model): validators=[importer.validators.validate_field_defaults], ) + field_filters = models.JSONField( + blank=True, + null=True, + verbose_name=_('Field Filters'), + validators=[importer.validators.validate_field_defaults], + ) + @property def field_mapping(self): """Construct a dict of field mappings for this import session. diff --git a/src/backend/InvenTree/importer/serializers.py b/src/backend/InvenTree/importer/serializers.py index 2400dc179d..ac68056f55 100644 --- a/src/backend/InvenTree/importer/serializers.py +++ b/src/backend/InvenTree/importer/serializers.py @@ -50,6 +50,7 @@ class DataImportSessionSerializer(InvenTreeModelSerializer): 'column_mappings', 'field_defaults', 'field_overrides', + 'field_filters', 'row_count', 'completed_row_count', ] @@ -104,6 +105,19 @@ class DataImportSessionSerializer(InvenTreeModelSerializer): return overrides + def validate_field_filters(self, filters): + """De-stringify the field filters.""" + if filters is None: + return None + + if type(filters) is not dict: + try: + filters = json.loads(str(filters)) + except: + raise ValidationError(_('Invalid field filters')) + + return filters + def create(self, validated_data): """Override create method for this serializer. diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index dd6129bec8..f8ed5c6f39 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -373,13 +373,13 @@ class PurchaseOrderLineItemSerializer( fields = [ 'pk', + 'part', 'quantity', 'reference', 'notes', 'order', 'order_detail', 'overdue', - 'part', 'part_detail', 'supplier_part_detail', 'received', @@ -454,6 +454,14 @@ class PurchaseOrderLineItemSerializer( return queryset + part = serializers.PrimaryKeyRelatedField( + queryset=part_models.SupplierPart.objects.all(), + many=False, + required=True, + allow_null=True, + label=_('Supplier Part'), + ) + quantity = serializers.FloatField(min_value=0, required=True) def validate_quantity(self, quantity): diff --git a/src/frontend/src/components/importer/ImportDataSelector.tsx b/src/frontend/src/components/importer/ImportDataSelector.tsx index 095a42bd59..9c6d7c531f 100644 --- a/src/frontend/src/components/importer/ImportDataSelector.tsx +++ b/src/frontend/src/components/importer/ImportDataSelector.tsx @@ -138,16 +138,27 @@ export default function ImporterDataSelector({ // Find the field definition in session.availableFields let fieldDef = session.availableFields[field]; if (fieldDef) { + // Construct field filters based on session field filters + let filters = fieldDef.filters ?? {}; + + if (session.fieldFilters[field]) { + filters = { + ...filters, + ...session.fieldFilters[field] + }; + } + fields[field] = { ...fieldDef, field_type: fieldDef.type, - description: fieldDef.help_text + description: fieldDef.help_text, + filters: filters }; } } return fields; - }, [selectedFieldNames, session.availableFields]); + }, [selectedFieldNames, session.availableFields, session.fieldFilters]); const importData = useCallback( (rows: number[]) => { diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 85c50ced5a..aa14af36d2 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -71,7 +71,7 @@ const RendererLookup: EnumDictionary< [ModelType.parttesttemplate]: RenderPartTestTemplate, [ModelType.projectcode]: RenderProjectCode, [ModelType.purchaseorder]: RenderPurchaseOrder, - [ModelType.purchaseorderline]: RenderPurchaseOrder, + [ModelType.purchaseorderlineitem]: RenderPurchaseOrder, [ModelType.returnorder]: RenderReturnOrder, [ModelType.salesorder]: RenderSalesOrder, [ModelType.salesordershipment]: RenderSalesOrderShipment, diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index e8b12da9ca..50a22c82ba 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -143,7 +143,7 @@ export const ModelInformationDict: ModelDict = { api_endpoint: ApiEndpoints.purchase_order_list, admin_url: '/order/purchaseorder/' }, - purchaseorderline: { + purchaseorderlineitem: { label: t`Purchase Order Line`, label_multiple: t`Purchase Order Lines`, api_endpoint: ApiEndpoints.purchase_order_line_list diff --git a/src/frontend/src/defaults/backendMappings.tsx b/src/frontend/src/defaults/backendMappings.tsx index 25dd8965e0..3339b27a8e 100644 --- a/src/frontend/src/defaults/backendMappings.tsx +++ b/src/frontend/src/defaults/backendMappings.tsx @@ -9,7 +9,7 @@ import { ModelType } from '../enums/ModelType'; export const statusCodeList: Record = { BuildStatus: ModelType.build, PurchaseOrderStatus: ModelType.purchaseorder, - ReturnOrderLineStatus: ModelType.purchaseorderline, + ReturnOrderLineStatus: ModelType.purchaseorderlineitem, ReturnOrderStatus: ModelType.returnorder, SalesOrderStatus: ModelType.salesorder, StockHistoryCode: ModelType.stockhistory, diff --git a/src/frontend/src/enums/ModelType.tsx b/src/frontend/src/enums/ModelType.tsx index e71944f954..570c382c25 100644 --- a/src/frontend/src/enums/ModelType.tsx +++ b/src/frontend/src/enums/ModelType.tsx @@ -18,7 +18,7 @@ export enum ModelType { builditem = 'builditem', company = 'company', purchaseorder = 'purchaseorder', - purchaseorderline = 'purchaseorderline', + purchaseorderlineitem = 'purchaseorderlineitem', salesorder = 'salesorder', salesordershipment = 'salesordershipment', returnorder = 'returnorder', diff --git a/src/frontend/src/forms/ImporterForms.tsx b/src/frontend/src/forms/ImporterForms.tsx index 9fac28cbf5..9ce00d30b6 100644 --- a/src/frontend/src/forms/ImporterForms.tsx +++ b/src/frontend/src/forms/ImporterForms.tsx @@ -11,6 +11,10 @@ export function dataImporterSessionFields(): ApiFormFieldSet { field_overrides: { hidden: true, value: {} + }, + field_filters: { + hidden: true, + value: {} } }; } diff --git a/src/frontend/src/hooks/UseImportSession.tsx b/src/frontend/src/hooks/UseImportSession.tsx index 34d52dff20..361206f5a7 100644 --- a/src/frontend/src/hooks/UseImportSession.tsx +++ b/src/frontend/src/hooks/UseImportSession.tsx @@ -31,6 +31,7 @@ export type ImportSessionState = { columnMappings: any[]; fieldDefaults: any; fieldOverrides: any; + fieldFilters: any; rowCount: number; completedRowCount: number; }; @@ -113,6 +114,10 @@ export function useImportSession({ return sessionData?.field_overrides ?? {}; }, [sessionData]); + const fieldFilters: any = useMemo(() => { + return sessionData?.field_filters ?? {}; + }, [sessionData]); + const rowCount: number = useMemo(() => { return sessionData?.row_count ?? 0; }, [sessionData]); @@ -134,6 +139,7 @@ export function useImportSession({ mappedFields, fieldDefaults, fieldOverrides, + fieldFilters, rowCount, completedRowCount }; diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index 5e83b22f2b..7cf646b8fb 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -242,6 +242,7 @@ export default function PurchaseOrderDetail() { icon: , content: ( diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx index f1bac0c23d..d583cff857 100644 --- a/src/frontend/src/tables/bom/BomTable.tsx +++ b/src/frontend/src/tables/bom/BomTable.tsx @@ -561,7 +561,7 @@ export function BomTable({ { setSelectedSession(undefined); setImportOpened(false); diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 3a4dba67da..8083aafc90 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -1,16 +1,19 @@ import { t } from '@lingui/macro'; import { Text } from '@mantine/core'; -import { IconSquareArrowRight } from '@tabler/icons-react'; +import { Action } from '@mdxeditor/editor'; +import { IconFileArrowLeft, IconSquareArrowRight } from '@tabler/icons-react'; import { useCallback, useMemo, useState } from 'react'; import { ActionButton } from '../../components/buttons/ActionButton'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { Thumbnail } from '../../components/images/Thumbnail'; +import ImporterDrawer from '../../components/importer/ImporterDrawer'; import { ProgressBar } from '../../components/items/ProgressBar'; import { RenderStockLocation } from '../../components/render/Stock'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; +import { dataImporterSessionFields } from '../../forms/ImporterForms'; import { usePurchaseOrderLineItemFields, useReceiveLineItems @@ -44,10 +47,12 @@ import { TableHoverCard } from '../TableHoverCard'; * Display a table of purchase order line items, for a specific order */ export function PurchaseOrderLineItemTable({ + order, orderId, supplierId, params }: { + order: any; orderId: number; supplierId?: number; params?: any; @@ -56,6 +61,49 @@ export function PurchaseOrderLineItemTable({ const user = useUserState(); + // Data import + const [importOpened, setImportOpened] = useState(false); + const [selectedSession, setSelectedSession] = useState( + undefined + ); + + const importSessionFields = useMemo(() => { + let fields = dataImporterSessionFields(); + + fields.model_type.hidden = true; + fields.model_type.value = ModelType.purchaseorderlineitem; + + // Specify override values for import + fields.field_overrides.value = { + order: orderId + }; + + // Specify default values based on the order data + fields.field_defaults.value = { + purchase_price_currency: + order?.order_currency || order?.supplier_detail?.currency || undefined + }; + + fields.field_filters.value = { + part: { + supplier: supplierId, + active: true + } + }; + + return fields; + }, [order, orderId, supplierId]); + + const importLineItems = useCreateApiFormModal({ + url: ApiEndpoints.import_session_list, + title: t`Import Line Items`, + fields: importSessionFields, + onFormSuccess: (response: any) => { + setSelectedSession(response.pk); + setImportOpened(true); + } + }); + const [singleRecord, setSingleRecord] = useState(null); const receiveLineItems = useReceiveLineItems({ @@ -277,6 +325,12 @@ export function PurchaseOrderLineItemTable({ // Custom table actions const tableActions = useMemo(() => { return [ +