mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 03:26:45 +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:
parent
9db5205f79
commit
ddc3cd32f5
@ -1,13 +1,18 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
v323 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9313
|
||||||
- Adds BulkUpdate support to the Part API endpoint
|
- Adds BulkUpdate support to the Part API endpoint
|
||||||
- Remove legacy API endpoint to set part category for multiple parts
|
- Remove legacy API endpoint to set part category for multiple parts
|
||||||
|
@ -21,7 +21,7 @@ import common.settings
|
|||||||
import company.models
|
import company.models
|
||||||
from generic.states.api import StatusView
|
from generic.states.api import StatusView
|
||||||
from importer.mixins import DataExportViewMixin
|
from importer.mixins import DataExportViewMixin
|
||||||
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
|
from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView
|
||||||
from InvenTree.filters import (
|
from InvenTree.filters import (
|
||||||
SEARCH_ORDER_FILTER,
|
SEARCH_ORDER_FILTER,
|
||||||
SEARCH_ORDER_FILTER_ALIAS,
|
SEARCH_ORDER_FILTER_ALIAS,
|
||||||
@ -1178,7 +1178,7 @@ class SalesOrderAllocationMixin:
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocationList(SalesOrderAllocationMixin, ListAPI):
|
class SalesOrderAllocationList(SalesOrderAllocationMixin, BulkUpdateMixin, ListAPI):
|
||||||
"""API endpoint for listing SalesOrderAllocation objects."""
|
"""API endpoint for listing SalesOrderAllocation objects."""
|
||||||
|
|
||||||
filterset_class = SalesOrderAllocationFilter
|
filterset_class = SalesOrderAllocationFilter
|
||||||
|
@ -226,7 +226,7 @@ class CategoryFilter(rest_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class CategoryList(CategoryMixin, DataExportViewMixin, ListCreateAPI):
|
class CategoryList(CategoryMixin, BulkUpdateMixin, DataExportViewMixin, ListCreateAPI):
|
||||||
"""API endpoint for accessing a list of PartCategory objects.
|
"""API endpoint for accessing a list of PartCategory objects.
|
||||||
|
|
||||||
- GET: Return 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 company.serializers import CompanySerializer
|
||||||
from generic.states.api import StatusView
|
from generic.states.api import StatusView
|
||||||
from importer.mixins import DataExportViewMixin
|
from importer.mixins import DataExportViewMixin
|
||||||
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
|
from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView
|
||||||
from InvenTree.filters import (
|
from InvenTree.filters import (
|
||||||
ORDER_FILTER_ALIAS,
|
ORDER_FILTER_ALIAS,
|
||||||
SEARCH_ORDER_FILTER,
|
SEARCH_ORDER_FILTER,
|
||||||
@ -365,7 +365,9 @@ class StockLocationMixin:
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class StockLocationList(DataExportViewMixin, StockLocationMixin, ListCreateAPI):
|
class StockLocationList(
|
||||||
|
DataExportViewMixin, BulkUpdateMixin, StockLocationMixin, ListCreateAPI
|
||||||
|
):
|
||||||
"""API endpoint for list view of StockLocation objects.
|
"""API endpoint for list view of StockLocation objects.
|
||||||
|
|
||||||
- GET: Return list of StockLocation objects
|
- GET: Return list of StockLocation objects
|
||||||
|
@ -43,7 +43,8 @@ export default defineConfig({
|
|||||||
INVENTREE_ADMIN_URL: 'test-admin',
|
INVENTREE_ADMIN_URL: 'test-admin',
|
||||||
INVENTREE_SITE_URL: 'http://localhost:8000',
|
INVENTREE_SITE_URL: 'http://localhost:8000',
|
||||||
INVENTREE_CORS_ORIGIN_ALLOW_ALL: 'True',
|
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/',
|
url: 'http://127.0.0.1:8000/api/',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
|
@ -213,6 +213,7 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
icon: <IconBookmark />,
|
icon: <IconBookmark />,
|
||||||
content: (
|
content: (
|
||||||
<SalesOrderAllocationTable
|
<SalesOrderAllocationTable
|
||||||
|
orderId={shipment.order}
|
||||||
shipmentId={shipment.pk}
|
shipmentId={shipment.pk}
|
||||||
showPartInfo
|
showPartInfo
|
||||||
allowEdit={isPending}
|
allowEdit={isPending}
|
||||||
|
@ -172,6 +172,12 @@ export default function Stock() {
|
|||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'sublocations',
|
||||||
|
label: t`Stock Locations`,
|
||||||
|
icon: <IconSitemap />,
|
||||||
|
content: <StockLocationTable parentId={id} />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'stock-items',
|
name: 'stock-items',
|
||||||
label: t`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',
|
name: 'default_parts',
|
||||||
label: t`Default Parts`,
|
label: t`Default Parts`,
|
||||||
|
@ -5,12 +5,15 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { YesNoButton } from '../../components/buttons/YesNoButton';
|
import { YesNoButton } from '../../components/buttons/YesNoButton';
|
||||||
|
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||||
import { ApiIcon } from '../../components/items/ApiIcon';
|
import { ApiIcon } from '../../components/items/ApiIcon';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { partCategoryFields } from '../../forms/PartForms';
|
import { partCategoryFields } from '../../forms/PartForms';
|
||||||
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import {
|
import {
|
||||||
|
useBulkEditApiFormModal,
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
@ -120,10 +123,38 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
|
|||||||
onFormSuccess: (record: any) => table.updateRecord(record)
|
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 tableActions = useMemo(() => {
|
||||||
const can_add = user.hasAddRole(UserRoles.part_category);
|
const can_add = user.hasAddRole(UserRoles.part_category);
|
||||||
|
const can_edit = user.hasChangeRole(UserRoles.part_category);
|
||||||
|
|
||||||
return [
|
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
|
<AddItemButton
|
||||||
key='add-part-category'
|
key='add-part-category'
|
||||||
tooltip={t`Add Part Category`}
|
tooltip={t`Add Part Category`}
|
||||||
@ -131,7 +162,7 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
|
|||||||
hidden={!can_add}
|
hidden={!can_add}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user]);
|
}, [user, table.hasSelectedRecords]);
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any): RowAction[] => {
|
(record: any): RowAction[] => {
|
||||||
@ -154,12 +185,14 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
|
|||||||
<>
|
<>
|
||||||
{newCategory.modal}
|
{newCategory.modal}
|
||||||
{editCategory.modal}
|
{editCategory.modal}
|
||||||
|
{setParent.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.category_list)}
|
url={apiUrl(ApiEndpoints.category_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
props={{
|
props={{
|
||||||
enableDownload: true,
|
enableDownload: true,
|
||||||
|
enableSelection: true,
|
||||||
params: {
|
params: {
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
top_level: parentId === undefined ? true : undefined
|
top_level: parentId === undefined ? true : undefined
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { IconTruckDelivery } from '@tabler/icons-react';
|
||||||
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
import { formatDate } from '../../defaults/formatters';
|
import { formatDate } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useSalesOrderAllocationFields } from '../../forms/SalesOrderForms';
|
import { useSalesOrderAllocationFields } from '../../forms/SalesOrderForms';
|
||||||
import {
|
import {
|
||||||
|
useBulkEditApiFormModal,
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
@ -244,16 +248,51 @@ export default function SalesOrderAllocationTable({
|
|||||||
[allowEdit, user]
|
[allowEdit, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableActions = useMemo(() => {
|
// A subset of the selected allocations, which can be assigned to a shipment
|
||||||
if (!allowEdit) {
|
const nonShippedAllocationIds: number[] = useMemo(() => {
|
||||||
return [];
|
// 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 [];
|
const setShipment = useBulkEditApiFormModal({
|
||||||
}, [allowEdit, user]);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{setShipment.modal}
|
||||||
{editAllocation.modal}
|
{editAllocation.modal}
|
||||||
{deleteAllocation.modal}
|
{deleteAllocation.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
@ -277,9 +316,10 @@ export default function SalesOrderAllocationTable({
|
|||||||
enableColumnSwitching: !isSubTable,
|
enableColumnSwitching: !isSubTable,
|
||||||
enableFilters: !isSubTable,
|
enableFilters: !isSubTable,
|
||||||
enableDownload: !isSubTable,
|
enableDownload: !isSubTable,
|
||||||
|
enableSelection: !isSubTable,
|
||||||
minHeight: isSubTable ? 100 : undefined,
|
minHeight: isSubTable ? 100 : undefined,
|
||||||
rowActions: rowActions,
|
rowActions: rowActions,
|
||||||
tableActions: tableActions,
|
tableActions: isSubTable ? undefined : tableActions,
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
modelField: modelField ?? 'order',
|
modelField: modelField ?? 'order',
|
||||||
modelType: modelTarget ?? ModelType.salesorder
|
modelType: modelTarget ?? ModelType.salesorder
|
||||||
|
@ -3,12 +3,15 @@ import { Group } from '@mantine/core';
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
|
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||||
import { ApiIcon } from '../../components/items/ApiIcon';
|
import { ApiIcon } from '../../components/items/ApiIcon';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { stockLocationFields } from '../../forms/StockForms';
|
import { stockLocationFields } from '../../forms/StockForms';
|
||||||
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import {
|
import {
|
||||||
|
useBulkEditApiFormModal,
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
@ -118,10 +121,38 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
|
|||||||
onFormSuccess: (record: any) => table.updateRecord(record)
|
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 tableActions = useMemo(() => {
|
||||||
const can_add = user.hasAddRole(UserRoles.stock_location);
|
const can_add = user.hasAddRole(UserRoles.stock_location);
|
||||||
|
const can_edit = user.hasChangeRole(UserRoles.stock_location);
|
||||||
|
|
||||||
return [
|
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
|
<AddItemButton
|
||||||
key='add-stock-location'
|
key='add-stock-location'
|
||||||
tooltip={t`Add Stock Location`}
|
tooltip={t`Add Stock Location`}
|
||||||
@ -129,7 +160,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
|
|||||||
hidden={!can_add}
|
hidden={!can_add}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user]);
|
}, [user, table.hasSelectedRecords]);
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any): RowAction[] => {
|
(record: any): RowAction[] => {
|
||||||
@ -152,6 +183,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
|
|||||||
<>
|
<>
|
||||||
{newLocation.modal}
|
{newLocation.modal}
|
||||||
{editLocation.modal}
|
{editLocation.modal}
|
||||||
|
{setParent.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.stock_location_list)}
|
url={apiUrl(ApiEndpoints.stock_location_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user