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:
parent
0cd493e96e
commit
f132970ff3
@ -1,13 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# 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."""
|
||||
|
||||
|
||||
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
|
||||
- Enables copying line items when duplicating an order
|
||||
|
||||
|
@ -436,6 +436,13 @@ class SupplierPriceBreakList(ListCreateAPI):
|
||||
serializer_class = SupplierPriceBreakSerializer
|
||||
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):
|
||||
"""Return serializer instance for this endpoint."""
|
||||
try:
|
||||
@ -468,6 +475,13 @@ class SupplierPriceBreakDetail(RetrieveUpdateDestroyAPI):
|
||||
queryset = SupplierPriceBreak.objects.all()
|
||||
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 = [
|
||||
path(
|
||||
|
@ -512,6 +512,13 @@ class SupplierPriceBreakSerializer(
|
||||
if not part_detail:
|
||||
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()
|
||||
|
||||
price = InvenTreeMoneySerializer(allow_null=True, required=True, label=_('Price'))
|
||||
|
@ -991,11 +991,30 @@ class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||
return queryset.filter(delivery_date=None)
|
||||
|
||||
|
||||
class SalesOrderShipmentList(ListCreateAPI):
|
||||
"""API list endpoint for SalesOrderShipment model."""
|
||||
class SalesOrderShipmentMixin:
|
||||
"""Mixin class for SalesOrderShipment endpoints."""
|
||||
|
||||
queryset = models.SalesOrderShipment.objects.all()
|
||||
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
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||
@ -1003,12 +1022,9 @@ class SalesOrderShipmentList(ListCreateAPI):
|
||||
ordering_fields = ['delivery_date', 'shipment_date']
|
||||
|
||||
|
||||
class SalesOrderShipmentDetail(RetrieveUpdateDestroyAPI):
|
||||
class SalesOrderShipmentDetail(SalesOrderShipmentMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API detail endpooint for SalesOrderShipment model."""
|
||||
|
||||
queryset = models.SalesOrderShipment.objects.all()
|
||||
serializer_class = serializers.SalesOrderShipmentSerializer
|
||||
|
||||
|
||||
class SalesOrderShipmentComplete(CreateAPI):
|
||||
"""API endpoint for completing (shipping) a SalesOrderShipment."""
|
||||
|
@ -1715,6 +1715,13 @@ class PartStocktakeReportList(ListAPI):
|
||||
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):
|
||||
"""API endpoint for manually generating a new PartStocktakeReport."""
|
||||
|
||||
@ -2184,6 +2191,11 @@ part_api_urls = [
|
||||
PartStocktakeReportGenerate.as_view(),
|
||||
name='api-part-stocktake-report-generate',
|
||||
),
|
||||
path(
|
||||
'<int:pk>/',
|
||||
PartStocktakeReportDetail.as_view(),
|
||||
name='api-part-stocktake-report-detail',
|
||||
),
|
||||
path(
|
||||
'',
|
||||
PartStocktakeReportList.as_view(),
|
||||
|
@ -1193,6 +1193,7 @@ class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializ
|
||||
|
||||
model = PartStocktakeReport
|
||||
fields = ['pk', 'date', 'report', 'part_count', 'user', 'user_detail']
|
||||
read_only_fields = ['date', 'report', 'part_count', 'user']
|
||||
|
||||
user_detail = InvenTree.serializers.UserSerializer(
|
||||
source='user', read_only=True, many=False
|
||||
|
@ -95,6 +95,8 @@ 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/',
|
||||
|
@ -202,3 +202,29 @@ export function usePartParameterFields({
|
||||
};
|
||||
}, [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: {}
|
||||
};
|
||||
}
|
||||
|
@ -6,10 +6,13 @@ import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react';
|
||||
* Show a notification that the feature is not yet implemented
|
||||
*/
|
||||
export function notYetImplemented() {
|
||||
notifications.hide('not-implemented');
|
||||
|
||||
notifications.show({
|
||||
title: t`Not implemented`,
|
||||
message: t`This feature is not yet implemented`,
|
||||
color: 'red'
|
||||
color: 'red',
|
||||
id: 'not-implemented'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconClipboardCheck,
|
||||
IconCoins,
|
||||
IconCpu,
|
||||
IconDevicesPc,
|
||||
@ -91,6 +92,8 @@ const LocationTypesTable = Loadable(
|
||||
lazy(() => import('../../../../tables/stock/LocationTypesTable'))
|
||||
);
|
||||
|
||||
const StocktakePanel = Loadable(lazy(() => import('./StocktakePanel')));
|
||||
|
||||
export default function AdminCenter() {
|
||||
const user = useUserState();
|
||||
|
||||
@ -162,6 +165,12 @@ export default function AdminCenter() {
|
||||
icon: <IconSitemap />,
|
||||
content: <PartCategoryTemplateTable />
|
||||
},
|
||||
{
|
||||
name: 'stocktake',
|
||||
label: t`Stocktake`,
|
||||
icon: <IconClipboardCheck />,
|
||||
content: <StocktakePanel />
|
||||
},
|
||||
{
|
||||
name: 'labels',
|
||||
label: t`Label Templates`,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -3,7 +3,6 @@ import { Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBellCog,
|
||||
IconCategory,
|
||||
IconClipboardCheck,
|
||||
IconCurrencyDollar,
|
||||
IconFileAnalytics,
|
||||
IconFingerprint,
|
||||
@ -225,12 +224,6 @@ export default function SystemSettings() {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'stocktake',
|
||||
label: t`Stocktake`,
|
||||
icon: <IconClipboardCheck />,
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'buildorders',
|
||||
label: t`Build Orders`,
|
||||
|
@ -2,7 +2,9 @@ import { t } from '@lingui/macro';
|
||||
import {
|
||||
Accordion,
|
||||
Alert,
|
||||
Center,
|
||||
Grid,
|
||||
Loader,
|
||||
Skeleton,
|
||||
Space,
|
||||
Stack,
|
||||
@ -80,7 +82,10 @@ import {
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import {
|
||||
useGlobalSettingsState,
|
||||
useUserSettingsState
|
||||
} from '../../states/SettingsState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { BomTable } from '../../tables/bom/BomTable';
|
||||
import { UsedInTable } from '../../tables/bom/UsedInTable';
|
||||
@ -99,6 +104,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable';
|
||||
import PartPricingPanel from './PartPricingPanel';
|
||||
import PartStocktakeDetail from './PartStocktakeDetail';
|
||||
|
||||
/**
|
||||
* Detail view for a single Part instance
|
||||
@ -112,6 +118,7 @@ export default function PartDetail() {
|
||||
const [treeOpen, setTreeOpen] = useState(false);
|
||||
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
const userSettings = useUserSettingsState();
|
||||
|
||||
const {
|
||||
instance: part,
|
||||
@ -550,7 +557,7 @@ export default function PartDetail() {
|
||||
name: 'stock',
|
||||
label: t`Stock`,
|
||||
icon: <IconPackages />,
|
||||
content: part.pk && (
|
||||
content: part.pk ? (
|
||||
<StockItemTable
|
||||
tableName="part-stock"
|
||||
allowAdd
|
||||
@ -558,6 +565,10 @@ export default function PartDetail() {
|
||||
part: part.pk
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Center>
|
||||
<Loader />
|
||||
</Center>
|
||||
)
|
||||
},
|
||||
{
|
||||
@ -680,17 +691,22 @@ export default function PartDetail() {
|
||||
hidden: !part.salable,
|
||||
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',
|
||||
label: t`Scheduling`,
|
||||
icon: <IconCalendarStats />,
|
||||
content: <PlaceholderPanel />
|
||||
},
|
||||
{
|
||||
name: 'stocktake',
|
||||
label: t`Stocktake`,
|
||||
icon: <IconClipboardList />,
|
||||
content: <PlaceholderPanel />
|
||||
content: <PlaceholderPanel />,
|
||||
hidden: !userSettings.isSet('DISPLAY_SCHEDULE_TAB')
|
||||
},
|
||||
{
|
||||
name: 'test_templates',
|
||||
@ -746,7 +762,7 @@ export default function PartDetail() {
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [id, part, user]);
|
||||
}, [id, part, user, globalSettings, userSettings]);
|
||||
|
||||
// Fetch information on part revision
|
||||
const partRevisionQuery = useQuery({
|
||||
|
200
src/frontend/src/pages/part/PartStocktakeDetail.tsx
Normal file
200
src/frontend/src/pages/part/PartStocktakeDetail.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -32,14 +32,16 @@ export default function SupplierPricingPanel({
|
||||
}, [table.records]);
|
||||
|
||||
const supplierPricingData = useMemo(() => {
|
||||
return table.records.map((record: any) => {
|
||||
return {
|
||||
quantity: record.quantity,
|
||||
supplier_price: record.price,
|
||||
unit_price: calculateSupplierPartUnitPrice(record),
|
||||
name: record.part_detail?.SKU
|
||||
};
|
||||
});
|
||||
return (
|
||||
table.records?.map((record: any) => {
|
||||
return {
|
||||
quantity: record.quantity,
|
||||
supplier_price: record.price,
|
||||
unit_price: calculateSupplierPartUnitPrice(record),
|
||||
name: record.part_detail?.SKU
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}, [table.records]);
|
||||
|
||||
return (
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
Box,
|
||||
Group,
|
||||
Indicator,
|
||||
LoadingOverlay,
|
||||
Space,
|
||||
Stack,
|
||||
Tooltip
|
||||
@ -711,12 +710,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
</Group>
|
||||
</Group>
|
||||
<Box pos="relative">
|
||||
<LoadingOverlay
|
||||
visible={
|
||||
tableOptionQuery.isLoading || tableOptionQuery.isFetching
|
||||
}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
withTableBorder
|
||||
withColumnBorders
|
||||
|
110
src/frontend/src/tables/settings/StocktakeReportTable.tsx
Normal file
110
src/frontend/src/tables/settings/StocktakeReportTable.tsx
Normal 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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -18,7 +18,7 @@ test('PUI - Parts', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'Suppliers' }).click();
|
||||
await page.getByRole('tab', { name: 'Purchase Orders' }).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: 'Notes' }).click();
|
||||
await page.getByRole('tab', { name: 'Related Parts' }).click();
|
||||
|
@ -29,7 +29,6 @@ test('PUI - Admin', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'Labels' }).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: 'Purchase Orders' }).click();
|
||||
await page.getByRole('tab', { name: 'Sales Orders' }).click();
|
||||
|
Loading…
x
Reference in New Issue
Block a user