2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 22:21:37 +00:00

[refactor] Stocktake -> Stock History (#10124)

* Remove STOCKTAKE ruleset

* Adjust wording of settings

* Cleanup

* Improve text for global settings

* Add BulkDeleteMixin to "stocktake" endpoint

* Frontend updates

* Migrations

- Remove field 'last_stocktake' from Part model
- Remove fields 'user' and 'note' from PartStocktake model
- Remove model PartStocktakeReport

* Frontend cleanup

* Rename global setting

* Rewrite stocktake functionality

* Cleanup

* Adds custom exporter for part stocktake data

* Frontend cleanup

* Bump API version

* Tweaks

* Frontend updates

* Fix unit tests

* Fix helper func

* Add docs

* Fix broken link

* Docs updates

* Adjust playwright tests

* Add unit testing for plugin

* Add unit testing for stock history creation

* Fix unit test
This commit is contained in:
Oliver
2025-08-06 08:02:56 +10:00
committed by GitHub
parent b31e16eb98
commit 5574e7cf6b
46 changed files with 599 additions and 1123 deletions

View File

@@ -118,8 +118,6 @@ export enum ApiEndpoints {
part_pricing_internal = 'part/internal-price/',
part_pricing_sale = 'part/sale-price/',
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_tree = 'part/category/tree/',
category_parameter_list = 'part/category/parameters/',

View File

@@ -12,8 +12,7 @@ export enum UserRoles {
return_order = 'return_order',
sales_order = 'sales_order',
stock = 'stock',
stock_location = 'stock_location',
stocktake = 'stocktake'
stock_location = 'stock_location'
}
/*
@@ -46,8 +45,6 @@ export function userRoleLabel(role: UserRoles): string {
return t`Stock Items`;
case UserRoles.stock_location:
return t`Stock Location`;
case UserRoles.stocktake:
return t`Stocktake`;
default:
return role as string;
}

View File

@@ -279,14 +279,3 @@ export function partStocktakeFields(): ApiFormFieldSet {
note: {}
};
}
export function generateStocktakeReportFields(): ApiFormFieldSet {
return {
part: {},
category: {},
location: {},
exclude_external: {},
generate_report: {},
update_parts: {}
};
}

View File

@@ -1,7 +1,6 @@
import { t } from '@lingui/core/macro';
import { Stack } from '@mantine/core';
import {
IconClipboardCheck,
IconCoins,
IconCpu,
IconDevicesPc,
@@ -103,8 +102,6 @@ const LocationTypesTable = Loadable(
lazy(() => import('../../../../tables/stock/LocationTypesTable'))
);
const StocktakePanel = Loadable(lazy(() => import('./StocktakePanel')));
export default function AdminCenter() {
const user = useUserState();
@@ -197,13 +194,6 @@ export default function AdminCenter() {
content: <PartCategoryTemplateTable />,
hidden: !user.hasViewRole(UserRoles.part_category)
},
{
name: 'stocktake',
label: t`Stocktake`,
icon: <IconClipboardCheck />,
content: <StocktakePanel />,
hidden: !user.hasViewRole(UserRoles.stocktake)
},
{
name: 'labels',
label: t`Label Templates`,

View File

@@ -1,31 +0,0 @@
import { Trans } from '@lingui/react/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,6 +3,7 @@ import { Skeleton, Stack } from '@mantine/core';
import {
IconBellCog,
IconCategory,
IconClipboardList,
IconCurrencyDollar,
IconFileAnalytics,
IconFingerprint,
@@ -242,6 +243,22 @@ export default function SystemSettings() {
/>
)
},
{
name: 'stock-history',
label: t`Stock History`,
icon: <IconClipboardList />,
content: (
<GlobalSettingList
keys={[
'STOCKTAKE_ENABLE',
'STOCKTAKE_EXCLUDE_EXTERNAL',
'STOCKTAKE_AUTO_DAYS',
'STOCKTAKE_DELETE_OLD_ENTRIES',
'STOCKTAKE_DELETE_DAYS'
]}
/>
)
},
{
name: 'buildorders',
label: t`Build Orders`,

View File

@@ -109,7 +109,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
import PartAllocationPanel from './PartAllocationPanel';
import PartPricingPanel from './PartPricingPanel';
import PartStocktakeDetail from './PartStocktakeDetail';
import PartStockHistoryDetail from './PartStockHistoryDetail';
import PartSupplierDetail from './PartSupplierDetail';
/**
@@ -909,9 +909,12 @@ export default function PartDetail() {
name: 'stocktake',
label: t`Stock History`,
icon: <IconClipboardList />,
content: part ? <PartStocktakeDetail partId={part.pk} /> : <Skeleton />,
content: part ? (
<PartStockHistoryDetail partId={part.pk} />
) : (
<Skeleton />
),
hidden:
!user.hasViewRole(UserRoles.stocktake) ||
!globalSettings.isSet('STOCKTAKE_ENABLE') ||
!userSettings.isSet('DISPLAY_STOCKTAKE_TAB')
},

View File

@@ -1,3 +1,8 @@
import { RowDeleteAction, RowEditAction } from '@lib/components/RowActions';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import type { TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { type ChartTooltipProps, LineChart } from '@mantine/charts';
import {
@@ -8,27 +13,17 @@ import {
SimpleGrid,
Text
} from '@mantine/core';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '@lib/components/AddItemButton';
import { RowDeleteAction, RowEditAction } from '@lib/components/RowActions';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import type { TableColumn } from '@lib/types/Tables';
import dayjs from 'dayjs';
import { useCallback, useMemo, useState } from 'react';
import { formatDate, formatPriceRange } from '../../defaults/formatters';
import { partStocktakeFields } from '../../forms/PartForms';
import {
generateStocktakeReportFields,
partStocktakeFields
} from '../../forms/PartForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import { DecimalColumn } from '../../tables/ColumnRenderers';
import { InvenTreeTable } from '../../tables/InvenTreeTable';
/*
@@ -67,7 +62,7 @@ function ChartTooltip({ label, payload }: Readonly<ChartTooltipProps>) {
);
}
export default function PartStocktakeDetail({
export default function PartStockHistoryDetail({
partId
}: Readonly<{ partId: number }>) {
const user = useUserState();
@@ -94,29 +89,19 @@ export default function PartStocktakeDetail({
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 [
{
DecimalColumn({
accessor: 'quantity',
sortable: false,
switchable: false
},
{
}),
DecimalColumn({
accessor: 'item_count',
title: t`Stock Items`,
switchable: true,
sortable: false
},
}),
{
accessor: 'cost',
title: t`Stock Value`,
@@ -129,38 +114,24 @@ export default function PartStocktakeDetail({
},
{
accessor: 'date',
sortable: false
},
{
accessor: 'note',
sortable: false
sortable: true,
switchable: false
}
];
}, []);
const tableActions = useMemo(() => {
return [
<AddItemButton
key='add'
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),
hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => {
setSelectedStocktake(record.pk);
editStocktakeEntry.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.stocktake),
hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => {
setSelectedStocktake(record.pk);
deleteStocktakeEntry.open();
@@ -207,7 +178,6 @@ export default function PartStocktakeDetail({
return (
<>
{generateReport.modal}
{editStocktakeEntry.modal}
{deleteStocktakeEntry.modal}
<SimpleGrid cols={{ base: 1, md: 2 }}>
@@ -216,12 +186,13 @@ export default function PartStocktakeDetail({
tableState={table}
columns={tableColumns}
props={{
enableSelection: true,
enableBulkDelete: true,
params: {
part: partId,
ordering: 'date'
},
rowActions: rowActions,
tableActions: tableActions
rowActions: rowActions
}}
/>
{table.isLoading ? (

View File

@@ -316,12 +316,6 @@ function partTableFilters(): TableFilter[] {
label: t`Subscribed`,
description: t`Filter by parts to which the user is subscribed`,
type: 'boolean'
},
{
name: 'stocktake',
label: t`Has Stocktake`,
description: t`Filter by parts which have stocktake information`,
type: 'boolean'
}
];
}

View File

@@ -1,111 +0,0 @@
import { t } from '@lingui/core/macro';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '@lib/components/AddItemButton';
import { type RowAction, RowDeleteAction } from '@lib/components/RowActions';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { TableColumn } from '@lib/types/Tables';
import { AttachmentLink } from '../../components/items/AttachmentLink';
import { RenderUser } from '../../components/render/User';
import { generateStocktakeReportFields } from '../../forms/PartForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { DateColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
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} />,
noContext: true
},
{
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

@@ -100,10 +100,10 @@ export const navigate = async (
/**
* CLick on the 'tab' element with the provided name
*/
export const loadTab = async (page, tabName) => {
export const loadTab = async (page, tabName, exact?) => {
await page
.getByLabel(/panel-tabs-/)
.getByRole('tab', { name: tabName })
.getByRole('tab', { name: tabName, exact: exact ?? false })
.click();
await page.waitForLoadState('networkidle');

View File

@@ -139,7 +139,8 @@ test('Settings - Global', async ({ browser, request }) => {
await loadTab(page, 'Barcodes');
await loadTab(page, 'Pricing');
await loadTab(page, 'Parts');
await loadTab(page, 'Stock');
await loadTab(page, 'Stock', true);
await loadTab(page, 'Stock History');
await loadTab(page, 'Notifications');
await page