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:
parent
9db5205f79
commit
ddc3cd32f5
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user