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

[PUI] Stocktake (#7704)

* Adjust playwright test

* Add StocktakeReport table in the "admin" section

* Allow deletion of individual stocktake reports

* Add placeholder buttons

* Adds placeholder panel for stocktake data

* Implement <PartStocktakeTable />

* Add modal to generate a new report

* Generate stocktake report from part table

* Adjust table value

* panel display tweaks

* Improve query efficiency for supplier price breaks

* Refator part stocktake detail panel

* Fix role checks

* Cleanup code

* Fix "double loader" in <InvenTreeTable />

* API efficiency improvements

* Bump API version

* Tweak playwright test

* Update playwright test

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Oliver 2024-09-21 18:29:59 +10:00 committed by GitHub
parent 0cd493e96e
commit f132970ff3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 479 additions and 42 deletions

View File

@ -1,13 +1,16 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 255 INVENTREE_API_VERSION = 256
"""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 = """
v256 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
- Adjustments for "stocktake" (stock history) API endpoints
v255 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145 v255 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
- Enables copying line items when duplicating an order - Enables copying line items when duplicating an order

View File

@ -436,6 +436,13 @@ class SupplierPriceBreakList(ListCreateAPI):
serializer_class = SupplierPriceBreakSerializer serializer_class = SupplierPriceBreakSerializer
filterset_class = SupplierPriceBreakFilter filterset_class = SupplierPriceBreakFilter
def get_queryset(self):
"""Return annotated queryset for the SupplierPriceBreak list endpoint."""
queryset = super().get_queryset()
queryset = SupplierPriceBreakSerializer.annotate_queryset(queryset)
return queryset
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint.""" """Return serializer instance for this endpoint."""
try: try:
@ -468,6 +475,13 @@ class SupplierPriceBreakDetail(RetrieveUpdateDestroyAPI):
queryset = SupplierPriceBreak.objects.all() queryset = SupplierPriceBreak.objects.all()
serializer_class = SupplierPriceBreakSerializer serializer_class = SupplierPriceBreakSerializer
def get_queryset(self):
"""Return annotated queryset for the SupplierPriceBreak list endpoint."""
queryset = super().get_queryset()
queryset = SupplierPriceBreakSerializer.annotate_queryset(queryset)
return queryset
manufacturer_part_api_urls = [ manufacturer_part_api_urls = [
path( path(

View File

@ -512,6 +512,13 @@ class SupplierPriceBreakSerializer(
if not part_detail: if not part_detail:
self.fields.pop('part_detail', None) self.fields.pop('part_detail', None)
@staticmethod
def annotate_queryset(queryset):
"""Prefetch related fields for the queryset."""
queryset = queryset.select_related('part', 'part__supplier', 'part__part')
return queryset
quantity = InvenTreeDecimalField() quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer(allow_null=True, required=True, label=_('Price')) price = InvenTreeMoneySerializer(allow_null=True, required=True, label=_('Price'))

View File

@ -991,11 +991,30 @@ class SalesOrderShipmentFilter(rest_filters.FilterSet):
return queryset.filter(delivery_date=None) return queryset.filter(delivery_date=None)
class SalesOrderShipmentList(ListCreateAPI): class SalesOrderShipmentMixin:
"""API list endpoint for SalesOrderShipment model.""" """Mixin class for SalesOrderShipment endpoints."""
queryset = models.SalesOrderShipment.objects.all() queryset = models.SalesOrderShipment.objects.all()
serializer_class = serializers.SalesOrderShipmentSerializer serializer_class = serializers.SalesOrderShipmentSerializer
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'order',
'order__customer',
'allocations',
'allocations__item',
'allocations__item__part',
)
return queryset
class SalesOrderShipmentList(SalesOrderShipmentMixin, ListCreateAPI):
"""API list endpoint for SalesOrderShipment model."""
filterset_class = SalesOrderShipmentFilter filterset_class = SalesOrderShipmentFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS filter_backends = SEARCH_ORDER_FILTER_ALIAS
@ -1003,12 +1022,9 @@ class SalesOrderShipmentList(ListCreateAPI):
ordering_fields = ['delivery_date', 'shipment_date'] ordering_fields = ['delivery_date', 'shipment_date']
class SalesOrderShipmentDetail(RetrieveUpdateDestroyAPI): class SalesOrderShipmentDetail(SalesOrderShipmentMixin, RetrieveUpdateDestroyAPI):
"""API detail endpooint for SalesOrderShipment model.""" """API detail endpooint for SalesOrderShipment model."""
queryset = models.SalesOrderShipment.objects.all()
serializer_class = serializers.SalesOrderShipmentSerializer
class SalesOrderShipmentComplete(CreateAPI): class SalesOrderShipmentComplete(CreateAPI):
"""API endpoint for completing (shipping) a SalesOrderShipment.""" """API endpoint for completing (shipping) a SalesOrderShipment."""

View File

@ -1715,6 +1715,13 @@ class PartStocktakeReportList(ListAPI):
ordering = '-pk' ordering = '-pk'
class PartStocktakeReportDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single PartStocktakeReport object."""
queryset = PartStocktakeReport.objects.all()
serializer_class = part_serializers.PartStocktakeReportSerializer
class PartStocktakeReportGenerate(CreateAPI): class PartStocktakeReportGenerate(CreateAPI):
"""API endpoint for manually generating a new PartStocktakeReport.""" """API endpoint for manually generating a new PartStocktakeReport."""
@ -2184,6 +2191,11 @@ part_api_urls = [
PartStocktakeReportGenerate.as_view(), PartStocktakeReportGenerate.as_view(),
name='api-part-stocktake-report-generate', name='api-part-stocktake-report-generate',
), ),
path(
'<int:pk>/',
PartStocktakeReportDetail.as_view(),
name='api-part-stocktake-report-detail',
),
path( path(
'', '',
PartStocktakeReportList.as_view(), PartStocktakeReportList.as_view(),

View File

@ -1193,6 +1193,7 @@ class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializ
model = PartStocktakeReport model = PartStocktakeReport
fields = ['pk', 'date', 'report', 'part_count', 'user', 'user_detail'] fields = ['pk', 'date', 'report', 'part_count', 'user', 'user_detail']
read_only_fields = ['date', 'report', 'part_count', 'user']
user_detail = InvenTree.serializers.UserSerializer( user_detail = InvenTree.serializers.UserSerializer(
source='user', read_only=True, many=False source='user', read_only=True, many=False

View File

@ -95,6 +95,8 @@ export enum ApiEndpoints {
part_pricing_internal = 'part/internal-price/', part_pricing_internal = 'part/internal-price/',
part_pricing_sale = 'part/sale-price/', part_pricing_sale = 'part/sale-price/',
part_stocktake_list = 'part/stocktake/', part_stocktake_list = 'part/stocktake/',
part_stocktake_report_list = 'part/stocktake/report/',
part_stocktake_report_generate = 'part/stocktake/report/generate/',
category_list = 'part/category/', category_list = 'part/category/',
category_tree = 'part/category/tree/', category_tree = 'part/category/tree/',
category_parameter_list = 'part/category/parameters/', category_parameter_list = 'part/category/parameters/',

View File

@ -202,3 +202,29 @@ export function usePartParameterFields({
}; };
}, [editTemplate, fieldType, choices]); }, [editTemplate, fieldType, choices]);
} }
export function partStocktakeFields(): ApiFormFieldSet {
return {
part: {
hidden: true
},
quantity: {},
item_count: {},
cost_min: {},
cost_min_currency: {},
cost_max: {},
cost_max_currency: {},
note: {}
};
}
export function generateStocktakeReportFields(): ApiFormFieldSet {
return {
part: {},
category: {},
location: {},
exclude_external: {},
generate_report: {},
update_parts: {}
};
}

View File

@ -6,10 +6,13 @@ import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react';
* Show a notification that the feature is not yet implemented * Show a notification that the feature is not yet implemented
*/ */
export function notYetImplemented() { export function notYetImplemented() {
notifications.hide('not-implemented');
notifications.show({ notifications.show({
title: t`Not implemented`, title: t`Not implemented`,
message: t`This feature is not yet implemented`, message: t`This feature is not yet implemented`,
color: 'red' color: 'red',
id: 'not-implemented'
}); });
} }

View File

@ -9,6 +9,7 @@ import {
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { import {
IconClipboardCheck,
IconCoins, IconCoins,
IconCpu, IconCpu,
IconDevicesPc, IconDevicesPc,
@ -91,6 +92,8 @@ const LocationTypesTable = Loadable(
lazy(() => import('../../../../tables/stock/LocationTypesTable')) lazy(() => import('../../../../tables/stock/LocationTypesTable'))
); );
const StocktakePanel = Loadable(lazy(() => import('./StocktakePanel')));
export default function AdminCenter() { export default function AdminCenter() {
const user = useUserState(); const user = useUserState();
@ -162,6 +165,12 @@ export default function AdminCenter() {
icon: <IconSitemap />, icon: <IconSitemap />,
content: <PartCategoryTemplateTable /> content: <PartCategoryTemplateTable />
}, },
{
name: 'stocktake',
label: t`Stocktake`,
icon: <IconClipboardCheck />,
content: <StocktakePanel />
},
{ {
name: 'labels', name: 'labels',
label: t`Label Templates`, label: t`Label Templates`,

View File

@ -0,0 +1,31 @@
import { Trans } from '@lingui/macro';
import { Divider, Stack } from '@mantine/core';
import { lazy } from 'react';
import { StylishText } from '../../../../components/items/StylishText';
import { GlobalSettingList } from '../../../../components/settings/SettingList';
import { Loadable } from '../../../../functions/loading';
const StocktakeReportTable = Loadable(
lazy(() => import('../../../../tables/settings/StocktakeReportTable'))
);
export default function StocktakePanel() {
return (
<Stack gap="xs">
<GlobalSettingList
keys={[
'STOCKTAKE_ENABLE',
'STOCKTAKE_EXCLUDE_EXTERNAL',
'STOCKTAKE_AUTO_DAYS',
'STOCKTAKE_DELETE_REPORT_DAYS'
]}
/>
<StylishText size="lg">
<Trans>Stocktake Reports</Trans>
</StylishText>
<Divider />
<StocktakeReportTable />
</Stack>
);
}

View File

@ -3,7 +3,6 @@ import { Skeleton, Stack } from '@mantine/core';
import { import {
IconBellCog, IconBellCog,
IconCategory, IconCategory,
IconClipboardCheck,
IconCurrencyDollar, IconCurrencyDollar,
IconFileAnalytics, IconFileAnalytics,
IconFingerprint, IconFingerprint,
@ -225,12 +224,6 @@ export default function SystemSettings() {
/> />
) )
}, },
{
name: 'stocktake',
label: t`Stocktake`,
icon: <IconClipboardCheck />,
content: <PlaceholderPanel />
},
{ {
name: 'buildorders', name: 'buildorders',
label: t`Build Orders`, label: t`Build Orders`,

View File

@ -2,7 +2,9 @@ import { t } from '@lingui/macro';
import { import {
Accordion, Accordion,
Alert, Alert,
Center,
Grid, Grid,
Loader,
Skeleton, Skeleton,
Space, Space,
Stack, Stack,
@ -80,7 +82,10 @@ import {
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState'; import {
useGlobalSettingsState,
useUserSettingsState
} from '../../states/SettingsState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { BomTable } from '../../tables/bom/BomTable'; import { BomTable } from '../../tables/bom/BomTable';
import { UsedInTable } from '../../tables/bom/UsedInTable'; import { UsedInTable } from '../../tables/bom/UsedInTable';
@ -99,6 +104,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
import { StockItemTable } from '../../tables/stock/StockItemTable'; import { StockItemTable } from '../../tables/stock/StockItemTable';
import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable'; import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable';
import PartPricingPanel from './PartPricingPanel'; import PartPricingPanel from './PartPricingPanel';
import PartStocktakeDetail from './PartStocktakeDetail';
/** /**
* Detail view for a single Part instance * Detail view for a single Part instance
@ -112,6 +118,7 @@ export default function PartDetail() {
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
const globalSettings = useGlobalSettingsState(); const globalSettings = useGlobalSettingsState();
const userSettings = useUserSettingsState();
const { const {
instance: part, instance: part,
@ -550,7 +557,7 @@ export default function PartDetail() {
name: 'stock', name: 'stock',
label: t`Stock`, label: t`Stock`,
icon: <IconPackages />, icon: <IconPackages />,
content: part.pk && ( content: part.pk ? (
<StockItemTable <StockItemTable
tableName="part-stock" tableName="part-stock"
allowAdd allowAdd
@ -558,6 +565,10 @@ export default function PartDetail() {
part: part.pk part: part.pk
}} }}
/> />
) : (
<Center>
<Loader />
</Center>
) )
}, },
{ {
@ -680,17 +691,22 @@ export default function PartDetail() {
hidden: !part.salable, hidden: !part.salable,
content: part.pk ? <SalesOrderTable partId={part.pk} /> : <Skeleton /> content: part.pk ? <SalesOrderTable partId={part.pk} /> : <Skeleton />
}, },
{
name: 'stocktake',
label: t`Stock History`,
icon: <IconClipboardList />,
content: part ? <PartStocktakeDetail partId={part.pk} /> : <Skeleton />,
hidden:
!user.hasViewRole(UserRoles.stocktake) ||
!globalSettings.isSet('STOCKTAKE_ENABLE') ||
!userSettings.isSet('DISPLAY_STOCKTAKE_TAB')
},
{ {
name: 'scheduling', name: 'scheduling',
label: t`Scheduling`, label: t`Scheduling`,
icon: <IconCalendarStats />, icon: <IconCalendarStats />,
content: <PlaceholderPanel /> content: <PlaceholderPanel />,
}, hidden: !userSettings.isSet('DISPLAY_SCHEDULE_TAB')
{
name: 'stocktake',
label: t`Stocktake`,
icon: <IconClipboardList />,
content: <PlaceholderPanel />
}, },
{ {
name: 'test_templates', name: 'test_templates',
@ -746,7 +762,7 @@ export default function PartDetail() {
) )
} }
]; ];
}, [id, part, user]); }, [id, part, user, globalSettings, userSettings]);
// Fetch information on part revision // Fetch information on part revision
const partRevisionQuery = useQuery({ const partRevisionQuery = useQuery({

View File

@ -0,0 +1,200 @@
import { t } from '@lingui/macro';
import { LineChart } from '@mantine/charts';
import { Center, Loader, SimpleGrid } from '@mantine/core';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { formatPriceRange } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import {
generateStocktakeReportFields,
partStocktakeFields
} from '../../forms/PartForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../../tables/Column';
import { InvenTreeTable } from '../../tables/InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../../tables/RowActions';
export default function PartStocktakeDetail({ partId }: { partId: number }) {
const user = useUserState();
const table = useTable('part-stocktake');
const stocktakeFields = useMemo(() => partStocktakeFields(), []);
const [selectedStocktake, setSelectedStocktake] = useState<
number | undefined
>(undefined);
const editStocktakeEntry = useEditApiFormModal({
pk: selectedStocktake,
url: ApiEndpoints.part_stocktake_list,
title: t`Edit Stocktake Entry`,
fields: stocktakeFields,
table: table
});
const deleteStocktakeEntry = useDeleteApiFormModal({
pk: selectedStocktake,
url: ApiEndpoints.part_stocktake_list,
title: t`Delete Stocktake Entry`,
table: table
});
const generateReport = useCreateApiFormModal({
url: ApiEndpoints.part_stocktake_report_generate,
title: t`Generate Stocktake Report`,
fields: generateStocktakeReportFields(),
initialData: {
part: partId
},
successMessage: t`Stocktake report scheduled`
});
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'quantity',
sortable: true,
switchable: false
},
{
accessor: 'item_count',
title: t`Stock Items`,
switchable: true,
sortable: true
},
{
accessor: 'cost',
title: t`Stock Value`,
render: (record: any) => {
return formatPriceRange(record.cost_min, record.cost_max, {
currency: record.cost_min_currency
});
}
},
{
accessor: 'date',
sortable: true
},
{
accessor: 'note'
}
];
}, []);
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`New Stocktake Report`}
onClick={() => generateReport.open()}
hidden={!user.hasAddRole(UserRoles.stocktake)}
/>
];
}, [user]);
const rowActions = useCallback(
(record: any) => {
return [
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.stocktake),
onClick: () => {
setSelectedStocktake(record.pk);
editStocktakeEntry.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.stocktake),
onClick: () => {
setSelectedStocktake(record.pk);
deleteStocktakeEntry.open();
}
})
];
},
[user]
);
const chartData = useMemo(() => {
let records =
table.records?.map((record: any) => {
return {
date: record.date,
quantity: record.quantity,
value_min: record.cost_min,
value_max: record.cost_max
};
}) ?? [];
// Sort records to ensure correct date order
records.sort((a, b) => {
return new Date(a.date) < new Date(b.date) ? -1 : 1;
});
return records;
}, [table.records]);
return (
<>
{generateReport.modal}
{editStocktakeEntry.modal}
{deleteStocktakeEntry.modal}
<SimpleGrid cols={2}>
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_stocktake_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
part: partId
},
rowActions: rowActions,
tableActions: tableActions
}}
/>
{table.isLoading ? (
<Center>
<Loader />
</Center>
) : (
<LineChart
data={chartData}
dataKey="date"
withLegend
withYAxis
withRightYAxis
yAxisLabel={t`Quantity`}
rightYAxisLabel={t`Stock Value`}
series={[
{
name: 'quantity',
label: t`Quantity`,
color: 'blue.6',
yAxisId: 'left'
},
{
name: 'value_min',
label: t`Min Value`,
color: 'teal.6',
yAxisId: 'right'
},
{
name: 'value_max',
label: t`Max Value`,
color: 'red.6',
yAxisId: 'right'
}
]}
/>
)}
</SimpleGrid>
</>
);
}

View File

@ -32,14 +32,16 @@ export default function SupplierPricingPanel({
}, [table.records]); }, [table.records]);
const supplierPricingData = useMemo(() => { const supplierPricingData = useMemo(() => {
return table.records.map((record: any) => { return (
return { table.records?.map((record: any) => {
quantity: record.quantity, return {
supplier_price: record.price, quantity: record.quantity,
unit_price: calculateSupplierPartUnitPrice(record), supplier_price: record.price,
name: record.part_detail?.SKU unit_price: calculateSupplierPartUnitPrice(record),
}; name: record.part_detail?.SKU
}); };
}) ?? []
);
}, [table.records]); }, [table.records]);
return ( return (

View File

@ -5,7 +5,6 @@ import {
Box, Box,
Group, Group,
Indicator, Indicator,
LoadingOverlay,
Space, Space,
Stack, Stack,
Tooltip Tooltip
@ -711,12 +710,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
</Group> </Group>
</Group> </Group>
<Box pos="relative"> <Box pos="relative">
<LoadingOverlay
visible={
tableOptionQuery.isLoading || tableOptionQuery.isFetching
}
/>
<DataTable <DataTable
withTableBorder withTableBorder
withColumnBorders withColumnBorders

View File

@ -0,0 +1,110 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { AttachmentLink } from '../../components/items/AttachmentLink';
import { RenderUser } from '../../components/render/User';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { generateStocktakeReportFields } from '../../forms/PartForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { TableColumn } from '../Column';
import { DateColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction, RowDeleteAction } from '../RowActions';
export default function StocktakeReportTable() {
const table = useTable('stocktake-report');
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'report',
title: t`Report`,
sortable: false,
switchable: false,
render: (record: any) => <AttachmentLink attachment={record.report} />
},
{
accessor: 'part_count',
title: t`Part Count`,
sortable: false
},
DateColumn({
accessor: 'date',
title: t`Date`
}),
{
accessor: 'user',
title: t`User`,
sortable: false,
render: (record: any) => RenderUser({ instance: record.user_detail })
}
];
}, []);
const [selectedReport, setSelectedReport] = useState<number | undefined>(
undefined
);
const deleteReport = useDeleteApiFormModal({
url: ApiEndpoints.part_stocktake_report_list,
pk: selectedReport,
title: t`Delete Report`,
onFormSuccess: () => table.refreshTable()
});
const generateFields: ApiFormFieldSet = useMemo(
() => generateStocktakeReportFields(),
[]
);
const generateReport = useCreateApiFormModal({
url: ApiEndpoints.part_stocktake_report_generate,
title: t`Generate Stocktake Report`,
fields: generateFields,
successMessage: t`Stocktake report scheduled`
});
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`New Stocktake Report`}
onClick={() => generateReport.open()}
/>
];
}, []);
const rowActions = useCallback((record: any): RowAction[] => {
return [
RowDeleteAction({
onClick: () => {
setSelectedReport(record.pk);
deleteReport.open();
}
})
];
}, []);
return (
<>
{generateReport.modal}
{deleteReport.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_stocktake_report_list)}
tableState={table}
columns={tableColumns}
props={{
enableSearch: false,
rowActions: rowActions,
tableActions: tableActions
}}
/>
</>
);
}

View File

@ -18,7 +18,7 @@ test('PUI - Parts', async ({ page }) => {
await page.getByRole('tab', { name: 'Suppliers' }).click(); await page.getByRole('tab', { name: 'Suppliers' }).click();
await page.getByRole('tab', { name: 'Purchase Orders' }).click(); await page.getByRole('tab', { name: 'Purchase Orders' }).click();
await page.getByRole('tab', { name: 'Scheduling' }).click(); await page.getByRole('tab', { name: 'Scheduling' }).click();
await page.getByRole('tab', { name: 'Stocktake' }).click(); await page.getByRole('tab', { name: 'Stock History' }).click();
await page.getByRole('tab', { name: 'Attachments' }).click(); await page.getByRole('tab', { name: 'Attachments' }).click();
await page.getByRole('tab', { name: 'Notes' }).click(); await page.getByRole('tab', { name: 'Notes' }).click();
await page.getByRole('tab', { name: 'Related Parts' }).click(); await page.getByRole('tab', { name: 'Related Parts' }).click();

View File

@ -29,7 +29,6 @@ test('PUI - Admin', async ({ page }) => {
await page.getByRole('tab', { name: 'Labels' }).click(); await page.getByRole('tab', { name: 'Labels' }).click();
await page.getByRole('tab', { name: 'Reporting' }).click(); await page.getByRole('tab', { name: 'Reporting' }).click();
await page.getByRole('tab', { name: 'Stocktake' }).click();
await page.getByRole('tab', { name: 'Build Orders' }).click(); await page.getByRole('tab', { name: 'Build Orders' }).click();
await page.getByRole('tab', { name: 'Purchase Orders' }).click(); await page.getByRole('tab', { name: 'Purchase Orders' }).click();
await page.getByRole('tab', { name: 'Sales Orders' }).click(); await page.getByRole('tab', { name: 'Sales Orders' }).click();