From ddc3cd32f5265179bce57aab50db9ea58e166347 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 17 Mar 2025 23:27:32 +1100 Subject: [PATCH] [UI] Bulk edit actions (#9320) * Allow bulk selection of sales order shipment * Tweaks * Support bulk-edit for location parent and category parent * Allow more login attempts for playwright --- .../InvenTree/InvenTree/api_version.py | 7 ++- src/backend/InvenTree/order/api.py | 4 +- src/backend/InvenTree/part/api.py | 2 +- src/backend/InvenTree/stock/api.py | 6 ++- src/frontend/playwright.config.ts | 3 +- .../pages/sales/SalesOrderShipmentDetail.tsx | 1 + .../src/pages/stock/LocationDetail.tsx | 12 ++--- .../src/tables/part/PartCategoryTable.tsx | 35 +++++++++++- .../sales/SalesOrderAllocationTable.tsx | 54 ++++++++++++++++--- .../src/tables/stock/StockLocationTable.tsx | 34 +++++++++++- 10 files changed, 136 insertions(+), 22 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 1a46da67e7..c8d211922f 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,18 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 323 +INVENTREE_API_VERSION = 324 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v324 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9320 + - Adds BulkUpdate support for the SalesOrderAllocation model + - Adds BulkUpdate support for the PartCategory model + - Adds BulkUpdate support for the StockLocation model + v323 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9313 - Adds BulkUpdate support to the Part API endpoint - Remove legacy API endpoint to set part category for multiple parts diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 71fc522886..0c2130a0d4 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -21,7 +21,7 @@ import common.settings import company.models from generic.states.api import StatusView from importer.mixins import DataExportViewMixin -from InvenTree.api import ListCreateDestroyAPIView, MetadataView +from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView from InvenTree.filters import ( SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS, @@ -1178,7 +1178,7 @@ class SalesOrderAllocationMixin: return queryset -class SalesOrderAllocationList(SalesOrderAllocationMixin, ListAPI): +class SalesOrderAllocationList(SalesOrderAllocationMixin, BulkUpdateMixin, ListAPI): """API endpoint for listing SalesOrderAllocation objects.""" filterset_class = SalesOrderAllocationFilter diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 22094273b3..f50a30f8b5 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -226,7 +226,7 @@ class CategoryFilter(rest_filters.FilterSet): return queryset -class CategoryList(CategoryMixin, DataExportViewMixin, ListCreateAPI): +class CategoryList(CategoryMixin, BulkUpdateMixin, DataExportViewMixin, ListCreateAPI): """API endpoint for accessing a list of PartCategory objects. - GET: Return a list of PartCategory objects diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 88637db49e..386b6d1bc2 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -27,7 +27,7 @@ from company.models import Company, SupplierPart from company.serializers import CompanySerializer from generic.states.api import StatusView from importer.mixins import DataExportViewMixin -from InvenTree.api import ListCreateDestroyAPIView, MetadataView +from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView from InvenTree.filters import ( ORDER_FILTER_ALIAS, SEARCH_ORDER_FILTER, @@ -365,7 +365,9 @@ class StockLocationMixin: return queryset -class StockLocationList(DataExportViewMixin, StockLocationMixin, ListCreateAPI): +class StockLocationList( + DataExportViewMixin, BulkUpdateMixin, StockLocationMixin, ListCreateAPI +): """API endpoint for list view of StockLocation objects. - GET: Return list of StockLocation objects diff --git a/src/frontend/playwright.config.ts b/src/frontend/playwright.config.ts index 663c533880..6a819597d8 100644 --- a/src/frontend/playwright.config.ts +++ b/src/frontend/playwright.config.ts @@ -43,7 +43,8 @@ export default defineConfig({ INVENTREE_ADMIN_URL: 'test-admin', INVENTREE_SITE_URL: 'http://localhost:8000', INVENTREE_CORS_ORIGIN_ALLOW_ALL: 'True', - INVENTREE_COOKIE_SAMESITE: 'Lax' + INVENTREE_COOKIE_SAMESITE: 'Lax', + INVENTREE_LOGIN_ATTEMPTS: '100' }, url: 'http://127.0.0.1:8000/api/', reuseExistingServer: !process.env.CI, diff --git a/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx b/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx index bcf40c747f..34cd1e0138 100644 --- a/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx @@ -213,6 +213,7 @@ export default function SalesOrderShipmentDetail() { icon: , content: ( , content: detailsPanel }, + { + name: 'sublocations', + label: t`Stock Locations`, + icon: , + content: + }, { name: 'stock-items', label: t`Stock Items`, @@ -186,12 +192,6 @@ export default function Stock() { /> ) }, - { - name: 'sublocations', - label: t`Stock Locations`, - icon: , - content: - }, { name: 'default_parts', label: t`Default Parts`, diff --git a/src/frontend/src/tables/part/PartCategoryTable.tsx b/src/frontend/src/tables/part/PartCategoryTable.tsx index be5ed601a1..04c93a1bb4 100644 --- a/src/frontend/src/tables/part/PartCategoryTable.tsx +++ b/src/frontend/src/tables/part/PartCategoryTable.tsx @@ -5,12 +5,15 @@ import { useCallback, useMemo, useState } from 'react'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { YesNoButton } from '../../components/buttons/YesNoButton'; +import { ActionDropdown } from '../../components/items/ActionDropdown'; import { ApiIcon } from '../../components/items/ApiIcon'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { partCategoryFields } from '../../forms/PartForms'; +import { InvenTreeIcon } from '../../functions/icons'; import { + useBulkEditApiFormModal, useCreateApiFormModal, useEditApiFormModal } from '../../hooks/UseForm'; @@ -120,10 +123,38 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) { onFormSuccess: (record: any) => table.updateRecord(record) }); + const setParent = useBulkEditApiFormModal({ + url: ApiEndpoints.category_list, + items: table.selectedIds, + title: t`Set Parent Category`, + fields: { + parent: {} + }, + onFormSuccess: table.refreshTable + }); + const tableActions = useMemo(() => { const can_add = user.hasAddRole(UserRoles.part_category); + const can_edit = user.hasChangeRole(UserRoles.part_category); return [ + } + disabled={!table.hasSelectedRecords} + actions={[ + { + name: t`Set Parent`, + icon: , + tooltip: t`Set parent category for the selected items`, + hidden: !can_edit, + disabled: !table.hasSelectedRecords, + onClick: () => { + setParent.open(); + } + } + ]} + />, ) { hidden={!can_add} /> ]; - }, [user]); + }, [user, table.hasSelectedRecords]); const rowActions = useCallback( (record: any): RowAction[] => { @@ -154,12 +185,14 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) { <> {newCategory.modal} {editCategory.modal} + {setParent.modal} { - if (!allowEdit) { - return []; - } + // A subset of the selected allocations, which can be assigned to a shipment + const nonShippedAllocationIds: number[] = useMemo(() => { + // Only allow allocations which have not been shipped + return ( + table.selectedRecords?.filter((record) => { + return !record.shipment_detail?.shipment_date; + }) ?? [] + ).map((record: any) => record.pk); + }, [table.selectedRecords]); - return []; - }, [allowEdit, user]); + const setShipment = useBulkEditApiFormModal({ + url: ApiEndpoints.sales_order_allocation_list, + items: nonShippedAllocationIds, + title: t`Assign to Shipment`, + fields: { + shipment: { + filters: { + order: orderId, + shipped: false + } + } + }, + onFormSuccess: table.refreshTable + }); + + const tableActions = useMemo(() => { + return [ + } + onClick={() => { + setShipment.open(); + }} + disabled={nonShippedAllocationIds.length == 0} + hidden={ + !orderId || !allowEdit || !user.hasChangeRole(UserRoles.sales_order) + } + // TODO: Hide if order is already shipped + /> + ]; + }, [allowEdit, nonShippedAllocationIds, orderId, user]); return ( <> + {setShipment.modal} {editAllocation.modal} {deleteAllocation.modal} ) { onFormSuccess: (record: any) => table.updateRecord(record) }); + const setParent = useBulkEditApiFormModal({ + url: ApiEndpoints.stock_location_list, + items: table.selectedIds, + title: t`Set Parent Location`, + fields: { + parent: {} + }, + onFormSuccess: table.refreshTable + }); + const tableActions = useMemo(() => { const can_add = user.hasAddRole(UserRoles.stock_location); + const can_edit = user.hasChangeRole(UserRoles.stock_location); return [ + } + disabled={!table.hasSelectedRecords} + actions={[ + { + name: t`Set Parent`, + icon: , + tooltip: t`Set parent location for the selected items`, + hidden: !can_edit, + disabled: !table.hasSelectedRecords, + onClick: () => { + setParent.open(); + } + } + ]} + />, ) { hidden={!can_add} /> ]; - }, [user]); + }, [user, table.hasSelectedRecords]); const rowActions = useCallback( (record: any): RowAction[] => { @@ -152,6 +183,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) { <> {newLocation.modal} {editLocation.modal} + {setParent.modal}