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:
@@ -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/',
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -279,14 +279,3 @@ export function partStocktakeFields(): ApiFormFieldSet {
|
||||
note: {}
|
||||
};
|
||||
}
|
||||
|
||||
export function generateStocktakeReportFields(): ApiFormFieldSet {
|
||||
return {
|
||||
part: {},
|
||||
category: {},
|
||||
location: {},
|
||||
exclude_external: {},
|
||||
generate_report: {},
|
||||
update_parts: {}
|
||||
};
|
||||
}
|
||||
|
@@ -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`,
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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`,
|
||||
|
@@ -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')
|
||||
},
|
||||
|
@@ -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 ? (
|
@@ -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'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@@ -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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -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');
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user