Build default location (#7160)

* Set build default location on save

* Update destination location in PUI form

* Set location for generated stock outputs

* Construct buildorderoutput fieldset

* Add "location" field to BuildOrderOutput serializer

* Create new build outputs from PUI

* Refacator StockItemTable

* Support serial_numbers field

* Add new API endpoints for build order operations

* Implement "build output complete" form

* Refactor common table

* Implement ScrapBuildOutput form

* Implement BuildOutputCancel form

* Update API version
This commit is contained in:
Oliver 2024-05-05 19:34:35 +10:00 committed by GitHub
parent 3c0bb7d959
commit ecc3b25464
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 533 additions and 58 deletions

View File

@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 195
INVENTREE_API_VERSION = 196
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v196 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7160
- Adds "location" field to BuildOutputComplete API endpoint
v195 - 2024-05-03 : https://github.com/inventree/InvenTree/pull/7153
- Fixes bug in BuildOrderCancel API endpoint

View File

@ -109,6 +109,12 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
self.validate_reference_field(self.reference)
self.reference_int = self.rebuild_reference_field(self.reference)
# On first save (i.e. creation), run some extra checks
if self.pk is None:
# Set the destination location (if not specified)
if not self.destination:
self.destination = self.part.get_default_location()
try:
super().save(*args, **kwargs)
except InvalidMove:
@ -682,10 +688,13 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
"""
user = kwargs.get('user', None)
batch = kwargs.get('batch', self.batch)
location = kwargs.get('location', self.destination)
location = kwargs.get('location', None)
serials = kwargs.get('serials', None)
auto_allocate = kwargs.get('auto_allocate', False)
if location is None:
location = self.destination or self.part.get_default_location()
"""
Determine if we can create a single output (with quantity > 0),
or multiple outputs (with quantity = 1)

View File

@ -286,6 +286,13 @@ class BuildOutputCreateSerializer(serializers.Serializer):
help_text=_('Enter serial numbers for build outputs'),
)
location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(),
label=_('Location'),
help_text=_('Stock location for build output'),
required=False, allow_null=True
)
def validate_serial_numbers(self, serial_numbers):
"""Clean the provided serial number string"""
serial_numbers = serial_numbers.strip()
@ -310,6 +317,11 @@ class BuildOutputCreateSerializer(serializers.Serializer):
quantity = data['quantity']
serial_numbers = data.get('serial_numbers', '')
if part.trackable and not serial_numbers:
raise ValidationError({
'serial_numbers': _('Serial numbers must be provided for trackable parts')
})
if serial_numbers:
try:
@ -346,19 +358,15 @@ class BuildOutputCreateSerializer(serializers.Serializer):
"""Generate the new build output(s)"""
data = self.validated_data
quantity = data['quantity']
batch_code = data.get('batch_code', '')
auto_allocate = data.get('auto_allocate', False)
build = self.get_build()
user = self.context['request'].user
build.create_build_output(
quantity,
data['quantity'],
serials=self.serials,
batch=batch_code,
auto_allocate=auto_allocate,
user=user,
batch=data.get('batch_code', ''),
location=data.get('location', None),
auto_allocate=data.get('auto_allocate', False),
user=self.context['request'].user,
)

View File

@ -87,7 +87,7 @@ export type ApiFormFieldType = {
description?: string;
preFieldContent?: JSX.Element;
postFieldContent?: JSX.Element;
onValueChange?: (value: any) => void;
onValueChange?: (value: any, record?: any) => void;
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
headers?: string[];
};

View File

@ -187,7 +187,7 @@ export function RelatedModelField({
setPk(_pk);
// Run custom callback for this field (if provided)
definition.onValueChange?.(_pk);
definition.onValueChange?.(_pk, value.data ?? {});
},
[field.onChange, definition]
);

View File

@ -53,6 +53,10 @@ export enum ApiEndpoints {
// Build API endpoints
build_order_list = 'build/',
build_order_cancel = 'build/:id/cancel/',
build_output_create = 'build/:id/create-output/',
build_output_complete = 'build/:id/complete/',
build_output_scrap = 'build/:id/scrap-outputs/',
build_output_delete = 'build/:id/delete-outputs/',
build_order_attachment_list = 'build/attachment/',
build_line_list = 'build/line/',
@ -64,6 +68,7 @@ export enum ApiEndpoints {
part_parameter_template_list = 'part/parameter/template/',
part_thumbs_list = 'part/thumbs/',
part_pricing_get = 'part/:id/pricing/',
part_serial_numbers = 'part/:id/serial-numbers/',
part_pricing_internal = 'part/internal-price/',
part_pricing_sale = 'part/sale-price/',
part_stocktake_list = 'part/stocktake/',

View File

@ -1,3 +1,5 @@
import { t } from '@lingui/macro';
import { ActionIcon, Alert, Stack, Text } from '@mantine/core';
import {
IconCalendar,
IconLink,
@ -7,9 +9,18 @@ import {
IconUser,
IconUsersGroup
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { DataTable } from 'mantine-datatable';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { InvenTreeIcon } from '../functions/icons';
import { useCreateApiFormModal } from '../hooks/UseForm';
import { apiUrl } from '../states/ApiState';
import { PartColumn, StatusColumn } from '../tables/ColumnRenderers';
/**
* Field set for BuildOrder forms
@ -19,6 +30,10 @@ export function useBuildOrderFields({
}: {
create: boolean;
}): ApiFormFieldSet {
const [destination, setDestination] = useState<number | null | undefined>(
null
);
return useMemo(() => {
return {
reference: {},
@ -26,6 +41,14 @@ export function useBuildOrderFields({
filters: {
assembly: true,
virtual: false
},
onValueChange(value: any, record?: any) {
// Adjust the destination location for the build order
if (record) {
setDestination(
record.default_location || record.category_default_location
);
}
}
},
title: {},
@ -51,7 +74,8 @@ export function useBuildOrderFields({
destination: {
filters: {
structural: false
}
},
value: destination
},
link: {
icon: <IconLink />
@ -66,5 +90,337 @@ export function useBuildOrderFields({
}
}
};
}, [create]);
}, [create, destination]);
}
export function useBuildOrderOutputFields({
build
}: {
build: any;
}): ApiFormFieldSet {
const trackable: boolean = useMemo(() => {
return build.part_detail?.trackable ?? false;
}, [build.part_detail]);
const [location, setLocation] = useState<number | null>(null);
useEffect(() => {
setLocation(build.location || build.part_detail?.default_location || null);
}, [build.location, build.part_detail]);
const [quantity, setQuantity] = useState<number>(0);
useEffect(() => {
let build_quantity = build.quantity ?? 0;
let build_complete = build.completed ?? 0;
setQuantity(Math.max(0, build_quantity - build_complete));
}, [build]);
const [serialPlaceholder, setSerialPlaceholder] = useState<string>('');
useEffect(() => {
if (trackable) {
api
.get(apiUrl(ApiEndpoints.part_serial_numbers, build.part_detail.pk))
.then((response: any) => {
if (response.data?.next) {
setSerialPlaceholder(
t`Next serial number` + ' - ' + response.data.next
);
} else if (response.data?.latest) {
setSerialPlaceholder(
t`Latest serial number` + ' - ' + response.data.latest
);
} else {
setSerialPlaceholder('');
}
})
.catch(() => {
setSerialPlaceholder('');
});
} else {
setSerialPlaceholder('');
}
}, [build, trackable]);
return useMemo(() => {
return {
quantity: {
value: quantity,
onValueChange: (value: any) => {
setQuantity(value);
}
},
serial_numbers: {
hidden: !trackable,
placeholder: serialPlaceholder
},
batch_code: {},
location: {
value: location,
onValueChange: (value: any) => {
setQuantity(value);
}
},
auto_allocate: {
hidden: !trackable
}
};
}, [quantity, serialPlaceholder, trackable]);
}
/*
* Construct a table of build outputs, for displaying at the top of a form
*/
function buildOutputFormTable(outputs: any[], onRemove: (output: any) => void) {
return (
<DataTable
idAccessor="pk"
records={outputs}
columns={[
{
accessor: 'part',
title: t`Part`,
render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'quantity',
title: t`Quantity`,
render: (record: any) => {
if (record.serial) {
return `# ${record.serial}`;
} else {
return record.quantity;
}
}
},
StatusColumn({ model: ModelType.stockitem, sortable: false }),
{
accessor: 'actions',
title: '',
render: (record: any) => (
<ActionButton
key={`remove-output-${record.pk}`}
tooltip={t`Remove output`}
icon={<InvenTreeIcon icon="cancel" />}
color="red"
onClick={() => onRemove(record)}
disabled={outputs.length <= 1}
/>
)
}
]}
/>
);
}
export function useCompleteBuildOutputsForm({
build,
outputs,
onFormSuccess
}: {
build: any;
outputs: any[];
onFormSuccess: (response: any) => void;
}) {
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
const [location, setLocation] = useState<number | null>(null);
useEffect(() => {
setSelectedOutputs(outputs);
}, [outputs]);
useEffect(() => {
if (location) {
return;
}
setLocation(
build.destination || build.part_detail?.default_location || null
);
}, [location, build.destination, build.part_detail]);
// Remove a selected output from the list
const removeOutput = useCallback(
(output: any) => {
setSelectedOutputs(
selectedOutputs.filter((item) => item.pk != output.pk)
);
},
[selectedOutputs]
);
const preFormContent = useMemo(() => {
return buildOutputFormTable(selectedOutputs, removeOutput);
}, [selectedOutputs, removeOutput]);
const buildOutputCompleteFields: ApiFormFieldSet = useMemo(() => {
return {
outputs: {
hidden: true,
value: selectedOutputs.map((output) => {
return {
output: output.pk
};
})
},
status: {},
location: {
filters: {
structural: false
},
value: location,
onValueChange: (value) => {
setLocation(value);
}
},
notes: {},
accept_incomplete_allocation: {}
};
}, [selectedOutputs, location]);
return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_complete, build.pk),
method: 'POST',
title: t`Complete Build Outputs`,
fields: buildOutputCompleteFields,
onFormSuccess: onFormSuccess,
preFormContent: preFormContent,
successMessage: t`Build outputs have been completed`
});
}
export function useScrapBuildOutputsForm({
build,
outputs,
onFormSuccess
}: {
build: any;
outputs: any[];
onFormSuccess: (response: any) => void;
}) {
const [location, setLocation] = useState<number | null>(null);
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
useEffect(() => {
setSelectedOutputs(outputs);
}, [outputs]);
// Remove a selected output from the list
const removeOutput = useCallback(
(output: any) => {
setSelectedOutputs(
selectedOutputs.filter((item) => item.pk != output.pk)
);
},
[selectedOutputs]
);
useEffect(() => {
if (location) {
return;
}
setLocation(
build.destination || build.part_detail?.default_location || null
);
}, [location, build.destination, build.part_detail]);
const preFormContent = useMemo(() => {
return buildOutputFormTable(selectedOutputs, removeOutput);
}, [selectedOutputs, removeOutput]);
const buildOutputScrapFields: ApiFormFieldSet = useMemo(() => {
return {
outputs: {
hidden: true,
value: selectedOutputs.map((output) => {
return {
output: output.pk,
quantity: output.quantity
};
})
},
location: {
value: location,
onValueChange: (value) => {
setLocation(value);
}
},
notes: {},
discard_allocations: {}
};
}, [location, selectedOutputs]);
return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_scrap, build.pk),
method: 'POST',
title: t`Scrap Build Outputs`,
fields: buildOutputScrapFields,
onFormSuccess: onFormSuccess,
preFormContent: preFormContent,
successMessage: t`Build outputs have been scrapped`
});
}
export function useCancelBuildOutputsForm({
build,
outputs,
onFormSuccess
}: {
build: any;
outputs: any[];
onFormSuccess: (response: any) => void;
}) {
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
useEffect(() => {
setSelectedOutputs(outputs);
}, [outputs]);
// Remove a selected output from the list
const removeOutput = useCallback(
(output: any) => {
setSelectedOutputs(
selectedOutputs.filter((item) => item.pk != output.pk)
);
},
[selectedOutputs]
);
const preFormContent = useMemo(() => {
return (
<Stack spacing="xs">
<Alert color="red" title={t`Cancel Build Outputs`}>
<Text>{t`Selected build outputs will be deleted`}</Text>
</Alert>
{buildOutputFormTable(selectedOutputs, removeOutput)}
</Stack>
);
}, [selectedOutputs, removeOutput]);
const buildOutputCancelFields: ApiFormFieldSet = useMemo(() => {
return {
outputs: {
hidden: true,
value: selectedOutputs.map((output) => {
return {
output: output.pk
};
})
}
};
}, [selectedOutputs]);
return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_delete, build.pk),
method: 'POST',
title: t`Cancel Build Outputs`,
fields: buildOutputCancelFields,
preFormContent: preFormContent,
onFormSuccess: onFormSuccess,
successMessage: t`Build outputs have been cancelled`
});
}

View File

@ -79,10 +79,9 @@ export function useTable(tableName: string): TableState {
setSelectedRecords([]);
}, []);
const hasSelectedRecords = useMemo(
() => selectedRecords.length > 0,
[selectedRecords]
);
const hasSelectedRecords = useMemo(() => {
return selectedRecords.length > 0;
}, [selectedRecords]);
// Total record count
const [recordCount, setRecordCount] = useState<number>(0);

View File

@ -220,11 +220,7 @@ export default function BuildDetail() {
name: 'incomplete-outputs',
label: t`Incomplete Outputs`,
icon: <IconClipboardList />,
content: build.pk ? (
<BuildOutputTable buildId={build.pk} partId={build.part} />
) : (
<Skeleton />
)
content: build.pk ? <BuildOutputTable build={build} /> : <Skeleton />
// TODO: Hide if build is complete
},
{
@ -233,6 +229,8 @@ export default function BuildDetail() {
icon: <IconClipboardCheck />,
content: (
<StockItemTable
allowAdd={false}
tableName="build-outputs"
params={{
build: id,
is_building: false
@ -246,6 +244,8 @@ export default function BuildDetail() {
icon: <IconList />,
content: (
<StockItemTable
allowAdd={false}
tableName="build-consumed"
params={{
consumed_by: id
}}

View File

@ -197,7 +197,11 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
icon: <IconPackages />,
hidden: !company?.is_manufacturer && !company?.is_supplier,
content: company?.pk && (
<StockItemTable params={{ company: company.pk }} />
<StockItemTable
allowAdd={false}
tableName="company-stock"
params={{ company: company.pk }}
/>
)
},
{
@ -222,7 +226,11 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
icon: <IconPackageExport />,
hidden: !company?.is_customer,
content: company?.pk ? (
<StockItemTable params={{ customer: company.pk }} />
<StockItemTable
allowAdd={false}
tableName="assigned-stock"
params={{ customer: company.pk }}
/>
) : (
<Skeleton />
)

View File

@ -486,6 +486,8 @@ export default function PartDetail() {
icon: <IconPackages />,
content: part.pk && (
<StockItemTable
tableName="part-stock"
allowAdd
params={{
part: part.pk
}}

View File

@ -258,6 +258,7 @@ export default function PurchaseOrderDetail() {
icon: <IconPackages />,
content: (
<StockItemTable
tableName="received-stock"
params={{
purchase_order: id
}}

View File

@ -156,6 +156,8 @@ export default function Stock() {
icon: <IconPackages />,
content: (
<StockItemTable
tableName="location-stock"
allowAdd
params={{
location: id
}}

View File

@ -301,7 +301,10 @@ export default function StockDetail() {
icon: <IconSitemap />,
hidden: (stockitem?.child_items ?? 0) == 0,
content: stockitem?.pk ? (
<StockItemTable params={{ ancestor: stockitem.pk }} />
<StockItemTable
tableName="child-stock"
params={{ ancestor: stockitem.pk }}
/>
) : (
<Skeleton />
)

View File

@ -170,10 +170,18 @@ export function ProjectCodeColumn(): TableColumn {
};
}
export function StatusColumn(model: ModelType) {
export function StatusColumn({
model,
sortable,
accessor
}: {
model: ModelType;
sortable?: boolean;
accessor?: string;
}) {
return {
accessor: 'status',
sortable: true,
accessor: accessor ?? 'status',
sortable: sortable ?? true,
render: TableStatusRenderer(model)
};
}

View File

@ -59,7 +59,7 @@ function buildOrderTableColumns(): TableColumn[] {
/>
)
},
StatusColumn(ModelType.build),
StatusColumn({ model: ModelType.build }),
ProjectCodeColumn(),
{
accessor: 'priority',

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { IconCircleCheck, IconCircleX } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { api } from '../../App';
import { ActionButton } from '../../components/buttons/ActionButton';
@ -11,7 +11,14 @@ import { ProgressBar } from '../../components/items/ProgressBar';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import {
useBuildOrderOutputFields,
useCancelBuildOutputsForm,
useCompleteBuildOutputsForm,
useScrapBuildOutputsForm
} from '../../forms/BuildForms';
import { InvenTreeIcon } from '../../functions/icons';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -26,19 +33,21 @@ type TestResultOverview = {
result: boolean;
};
export default function BuildOutputTable({
buildId,
partId
}: {
buildId: number;
partId: number;
}) {
export default function BuildOutputTable({ build }: { build: any }) {
const user = useUserState();
const table = useTable('build-outputs');
const buildId: number = useMemo(() => {
return build.pk ?? -1;
}, [build.pk]);
const partId: number = useMemo(() => {
return build.part ?? -1;
}, [build.part]);
// Fetch the test templates associated with the partId
const { data: testTemplates } = useQuery({
queryKey: ['buildoutputtests', partId],
queryKey: ['buildoutputtests', build.part],
queryFn: async () => {
if (!partId) {
return [];
@ -98,36 +107,82 @@ export default function BuildOutputTable({
[partId, testTemplates]
);
const buildOutputFields = useBuildOrderOutputFields({ build: build });
const addBuildOutput = useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_create, buildId),
title: t`Add Build Output`,
fields: buildOutputFields,
onFormSuccess: () => {
table.refreshTable();
}
});
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
const completeBuildOutputsForm = useCompleteBuildOutputsForm({
build: build,
outputs: selectedOutputs,
onFormSuccess: () => {
table.refreshTable();
}
});
const scrapBuildOutputsForm = useScrapBuildOutputsForm({
build: build,
outputs: selectedOutputs,
onFormSuccess: () => {
table.refreshTable();
}
});
const cancelBuildOutputsForm = useCancelBuildOutputsForm({
build: build,
outputs: selectedOutputs,
onFormSuccess: () => {
table.refreshTable();
}
});
const tableActions = useMemo(() => {
// TODO: Button to create new build output
// TODO: Button to complete output(s)
// TODO: Button to cancel output(s)
// TODO: Button to scrap output(s)
return [
<AddItemButton
tooltip={t`Add Build Output`}
hidden={!user.hasAddRole(UserRoles.build)}
onClick={addBuildOutput.open}
/>,
<ActionButton
tooltip={t`Complete selected outputs`}
icon={<InvenTreeIcon icon="success" />}
color="green"
disabled={!table.hasSelectedRecords}
onClick={() => {
setSelectedOutputs(table.selectedRecords);
completeBuildOutputsForm.open();
}}
/>,
<ActionButton
tooltip={t`Scrap selected outputs`}
icon={<InvenTreeIcon icon="delete" />}
color="red"
disabled={!table.hasSelectedRecords}
onClick={() => {
setSelectedOutputs(table.selectedRecords);
scrapBuildOutputsForm.open();
}}
/>,
<ActionButton
tooltip={t`Cancel selected outputs`}
icon={<InvenTreeIcon icon="cancel" />}
color="red"
disabled={!table.hasSelectedRecords}
onClick={() => {
setSelectedOutputs(table.selectedRecords);
cancelBuildOutputsForm.open();
}}
/>
];
}, [user, partId, buildId, table.hasSelectedRecords]);
}, [user, table.selectedRecords, table.hasSelectedRecords]);
const rowActions = useCallback(
(record: any) => {
@ -148,25 +203,37 @@ export default function BuildOutputTable({
title: t`Complete`,
tooltip: t`Complete build output`,
color: 'green',
icon: <InvenTreeIcon icon="success" />
icon: <InvenTreeIcon icon="success" />,
onClick: () => {
setSelectedOutputs([record]);
completeBuildOutputsForm.open();
}
},
{
title: t`Scrap`,
tooltip: t`Scrap build output`,
icon: <InvenTreeIcon icon="delete" />,
color: 'red'
color: 'red',
onClick: () => {
setSelectedOutputs([record]);
scrapBuildOutputsForm.open();
}
},
{
title: t`Cancel`,
tooltip: t`Cancel build output`,
icon: <InvenTreeIcon icon="cancel" />,
color: 'red'
color: 'red',
onClick: () => {
setSelectedOutputs([record]);
cancelBuildOutputsForm.open();
}
}
];
return actions;
},
[user, partId, buildId]
[user, partId]
);
const tableColumns: TableColumn[] = useMemo(() => {
@ -257,6 +324,10 @@ export default function BuildOutputTable({
return (
<>
{addBuildOutput.modal}
{completeBuildOutputsForm.modal}
{scrapBuildOutputsForm.modal}
{cancelBuildOutputsForm.modal}
<InvenTreeTable
tableState={table}
url={apiUrl(ApiEndpoints.stock_item_list)}

View File

@ -103,7 +103,7 @@ export function PurchaseOrderTable({
accessor: 'supplier_reference'
},
LineItemsProgressColumn(),
StatusColumn(ModelType.purchaseorder),
StatusColumn({ model: ModelType.purchaseorder }),
ProjectCodeColumn(),
CreationDateColumn(),
TargetDateColumn(),

View File

@ -94,7 +94,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
},
DescriptionColumn({}),
LineItemsProgressColumn(),
StatusColumn(ModelType.returnorder),
StatusColumn({ model: ModelType.returnorder }),
ProjectCodeColumn(),
CreationDateColumn(),
TargetDateColumn(),

View File

@ -124,7 +124,7 @@ export function SalesOrderTable({
},
DescriptionColumn({}),
LineItemsProgressColumn(),
StatusColumn(ModelType.salesorder),
StatusColumn({ model: ModelType.salesorder }),
ProjectCodeColumn(),
CreationDateColumn(),
TargetDateColumn(),

View File

@ -41,7 +41,7 @@ export default function InstalledItemsTable({
accessor: 'batch',
switchable: false
},
StatusColumn(ModelType.stockitem)
StatusColumn({ model: ModelType.stockitem })
];
}, []);

View File

@ -193,7 +193,7 @@ function stockItemTableColumns(): TableColumn[] {
);
}
},
StatusColumn(ModelType.stockitem),
StatusColumn({ model: ModelType.stockitem }),
{
accessor: 'batch',
sortable: true
@ -336,12 +336,12 @@ function stockItemTableFilters(): TableFilter[] {
*/
export function StockItemTable({
params = {},
allowAdd = true,
allowAdd = false,
tableName = 'stockitems'
}: {
params?: any;
allowAdd?: boolean;
tableName?: string;
tableName: string;
}) {
let tableColumns = useMemo(() => stockItemTableColumns(), []);
let tableFilters = useMemo(() => stockItemTableFilters(), []);
@ -482,7 +482,7 @@ export function StockItemTable({
onClick={() => newStockItem.open()}
/>
];
}, [user, table]);
}, [user, table, allowAdd]);
return (
<>