2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-21 06:16:29 +00:00

[UI] Bulk edit actions ()

* 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
10 changed files with 136 additions and 22 deletions

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

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

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

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

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

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

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

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

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

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