2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-27 19:16:44 +00:00

[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
This commit is contained in:
Oliver 2025-03-17 23:27:32 +11:00 committed by GitHub
parent 9db5205f79
commit ddc3cd32f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 136 additions and 22 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -213,6 +213,7 @@ export default function SalesOrderShipmentDetail() {
icon: <IconBookmark />,
content: (
<SalesOrderAllocationTable
orderId={shipment.order}
shipmentId={shipment.pk}
showPartInfo
allowEdit={isPending}

View File

@ -172,6 +172,12 @@ export default function Stock() {
icon: <IconInfoCircle />,
content: detailsPanel
},
{
name: 'sublocations',
label: t`Stock Locations`,
icon: <IconSitemap />,
content: <StockLocationTable parentId={id} />
},
{
name: 'stock-items',
label: t`Stock Items`,
@ -186,12 +192,6 @@ export default function Stock() {
/>
)
},
{
name: 'sublocations',
label: t`Stock Locations`,
icon: <IconSitemap />,
content: <StockLocationTable parentId={id} />
},
{
name: 'default_parts',
label: t`Default Parts`,

View File

@ -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 [
<ActionDropdown
tooltip={t`Category Actions`}
icon={<InvenTreeIcon icon='category' />}
disabled={!table.hasSelectedRecords}
actions={[
{
name: t`Set Parent`,
icon: <InvenTreeIcon icon='category' />,
tooltip: t`Set parent category for the selected items`,
hidden: !can_edit,
disabled: !table.hasSelectedRecords,
onClick: () => {
setParent.open();
}
}
]}
/>,
<AddItemButton
key='add-part-category'
tooltip={t`Add Part Category`}
@ -131,7 +162,7 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
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}
<InvenTreeTable
url={apiUrl(ApiEndpoints.category_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: true,
enableSelection: true,
params: {
parent: parentId,
top_level: parentId === undefined ? true : undefined

View File

@ -1,11 +1,15 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo, useState } from 'react';
import { IconTruckDelivery } from '@tabler/icons-react';
import { ActionButton } from '../../components/buttons/ActionButton';
import { formatDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSalesOrderAllocationFields } from '../../forms/SalesOrderForms';
import {
useBulkEditApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
@ -244,16 +248,51 @@ export default function SalesOrderAllocationTable({
[allowEdit, user]
);
const tableActions = useMemo(() => {
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 [
<ActionButton
tooltip={t`Assign to shipment`}
icon={<IconTruckDelivery />}
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}
<InvenTreeTable
@ -277,9 +316,10 @@ export default function SalesOrderAllocationTable({
enableColumnSwitching: !isSubTable,
enableFilters: !isSubTable,
enableDownload: !isSubTable,
enableSelection: !isSubTable,
minHeight: isSubTable ? 100 : undefined,
rowActions: rowActions,
tableActions: tableActions,
tableActions: isSubTable ? undefined : tableActions,
tableFilters: tableFilters,
modelField: modelField ?? 'order',
modelType: modelTarget ?? ModelType.salesorder

View File

@ -3,12 +3,15 @@ import { Group } from '@mantine/core';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
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 { stockLocationFields } from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import {
useBulkEditApiFormModal,
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
@ -118,10 +121,38 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
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 [
<ActionDropdown
tooltip={t`Location Actions`}
icon={<InvenTreeIcon icon='location' />}
disabled={!table.hasSelectedRecords}
actions={[
{
name: t`Set Parent`,
icon: <InvenTreeIcon icon='location' />,
tooltip: t`Set parent location for the selected items`,
hidden: !can_edit,
disabled: !table.hasSelectedRecords,
onClick: () => {
setParent.open();
}
}
]}
/>,
<AddItemButton
key='add-stock-location'
tooltip={t`Add Stock Location`}
@ -129,7 +160,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
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}
<InvenTreeTable
url={apiUrl(ApiEndpoints.stock_location_list)}
tableState={table}