mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
3c0bb7d959
commit
ecc3b25464
@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""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 = """
|
||||||
|
|
||||||
|
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
|
v195 - 2024-05-03 : https://github.com/inventree/InvenTree/pull/7153
|
||||||
- Fixes bug in BuildOrderCancel API endpoint
|
- Fixes bug in BuildOrderCancel API endpoint
|
||||||
|
|
||||||
|
@ -109,6 +109,12 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
|
|||||||
self.validate_reference_field(self.reference)
|
self.validate_reference_field(self.reference)
|
||||||
self.reference_int = self.rebuild_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:
|
try:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
except InvalidMove:
|
except InvalidMove:
|
||||||
@ -682,10 +688,13 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
|
|||||||
"""
|
"""
|
||||||
user = kwargs.get('user', None)
|
user = kwargs.get('user', None)
|
||||||
batch = kwargs.get('batch', self.batch)
|
batch = kwargs.get('batch', self.batch)
|
||||||
location = kwargs.get('location', self.destination)
|
location = kwargs.get('location', None)
|
||||||
serials = kwargs.get('serials', None)
|
serials = kwargs.get('serials', None)
|
||||||
auto_allocate = kwargs.get('auto_allocate', False)
|
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),
|
Determine if we can create a single output (with quantity > 0),
|
||||||
or multiple outputs (with quantity = 1)
|
or multiple outputs (with quantity = 1)
|
||||||
|
@ -286,6 +286,13 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
help_text=_('Enter serial numbers for build outputs'),
|
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):
|
def validate_serial_numbers(self, serial_numbers):
|
||||||
"""Clean the provided serial number string"""
|
"""Clean the provided serial number string"""
|
||||||
serial_numbers = serial_numbers.strip()
|
serial_numbers = serial_numbers.strip()
|
||||||
@ -310,6 +317,11 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
quantity = data['quantity']
|
quantity = data['quantity']
|
||||||
serial_numbers = data.get('serial_numbers', '')
|
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:
|
if serial_numbers:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -346,19 +358,15 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
"""Generate the new build output(s)"""
|
"""Generate the new build output(s)"""
|
||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
|
|
||||||
quantity = data['quantity']
|
|
||||||
batch_code = data.get('batch_code', '')
|
|
||||||
auto_allocate = data.get('auto_allocate', False)
|
|
||||||
|
|
||||||
build = self.get_build()
|
build = self.get_build()
|
||||||
user = self.context['request'].user
|
|
||||||
|
|
||||||
build.create_build_output(
|
build.create_build_output(
|
||||||
quantity,
|
data['quantity'],
|
||||||
serials=self.serials,
|
serials=self.serials,
|
||||||
batch=batch_code,
|
batch=data.get('batch_code', ''),
|
||||||
auto_allocate=auto_allocate,
|
location=data.get('location', None),
|
||||||
user=user,
|
auto_allocate=data.get('auto_allocate', False),
|
||||||
|
user=self.context['request'].user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ export type ApiFormFieldType = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
preFieldContent?: JSX.Element;
|
preFieldContent?: JSX.Element;
|
||||||
postFieldContent?: JSX.Element;
|
postFieldContent?: JSX.Element;
|
||||||
onValueChange?: (value: any) => void;
|
onValueChange?: (value: any, record?: any) => void;
|
||||||
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
|
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
|
||||||
headers?: string[];
|
headers?: string[];
|
||||||
};
|
};
|
||||||
|
@ -187,7 +187,7 @@ export function RelatedModelField({
|
|||||||
setPk(_pk);
|
setPk(_pk);
|
||||||
|
|
||||||
// Run custom callback for this field (if provided)
|
// Run custom callback for this field (if provided)
|
||||||
definition.onValueChange?.(_pk);
|
definition.onValueChange?.(_pk, value.data ?? {});
|
||||||
},
|
},
|
||||||
[field.onChange, definition]
|
[field.onChange, definition]
|
||||||
);
|
);
|
||||||
|
@ -53,6 +53,10 @@ export enum ApiEndpoints {
|
|||||||
// Build API endpoints
|
// Build API endpoints
|
||||||
build_order_list = 'build/',
|
build_order_list = 'build/',
|
||||||
build_order_cancel = 'build/:id/cancel/',
|
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_order_attachment_list = 'build/attachment/',
|
||||||
build_line_list = 'build/line/',
|
build_line_list = 'build/line/',
|
||||||
|
|
||||||
@ -64,6 +68,7 @@ export enum ApiEndpoints {
|
|||||||
part_parameter_template_list = 'part/parameter/template/',
|
part_parameter_template_list = 'part/parameter/template/',
|
||||||
part_thumbs_list = 'part/thumbs/',
|
part_thumbs_list = 'part/thumbs/',
|
||||||
part_pricing_get = 'part/:id/pricing/',
|
part_pricing_get = 'part/:id/pricing/',
|
||||||
|
part_serial_numbers = 'part/:id/serial-numbers/',
|
||||||
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/',
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { ActionIcon, Alert, Stack, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconLink,
|
IconLink,
|
||||||
@ -7,9 +9,18 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconUsersGroup
|
IconUsersGroup
|
||||||
} from '@tabler/icons-react';
|
} 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 { 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
|
* Field set for BuildOrder forms
|
||||||
@ -19,6 +30,10 @@ export function useBuildOrderFields({
|
|||||||
}: {
|
}: {
|
||||||
create: boolean;
|
create: boolean;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
|
const [destination, setDestination] = useState<number | null | undefined>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
reference: {},
|
reference: {},
|
||||||
@ -26,6 +41,14 @@ export function useBuildOrderFields({
|
|||||||
filters: {
|
filters: {
|
||||||
assembly: true,
|
assembly: true,
|
||||||
virtual: false
|
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: {},
|
title: {},
|
||||||
@ -51,7 +74,8 @@ export function useBuildOrderFields({
|
|||||||
destination: {
|
destination: {
|
||||||
filters: {
|
filters: {
|
||||||
structural: false
|
structural: false
|
||||||
}
|
},
|
||||||
|
value: destination
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
icon: <IconLink />
|
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`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -79,10 +79,9 @@ export function useTable(tableName: string): TableState {
|
|||||||
setSelectedRecords([]);
|
setSelectedRecords([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hasSelectedRecords = useMemo(
|
const hasSelectedRecords = useMemo(() => {
|
||||||
() => selectedRecords.length > 0,
|
return selectedRecords.length > 0;
|
||||||
[selectedRecords]
|
}, [selectedRecords]);
|
||||||
);
|
|
||||||
|
|
||||||
// Total record count
|
// Total record count
|
||||||
const [recordCount, setRecordCount] = useState<number>(0);
|
const [recordCount, setRecordCount] = useState<number>(0);
|
||||||
|
@ -220,11 +220,7 @@ export default function BuildDetail() {
|
|||||||
name: 'incomplete-outputs',
|
name: 'incomplete-outputs',
|
||||||
label: t`Incomplete Outputs`,
|
label: t`Incomplete Outputs`,
|
||||||
icon: <IconClipboardList />,
|
icon: <IconClipboardList />,
|
||||||
content: build.pk ? (
|
content: build.pk ? <BuildOutputTable build={build} /> : <Skeleton />
|
||||||
<BuildOutputTable buildId={build.pk} partId={build.part} />
|
|
||||||
) : (
|
|
||||||
<Skeleton />
|
|
||||||
)
|
|
||||||
// TODO: Hide if build is complete
|
// TODO: Hide if build is complete
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -233,6 +229,8 @@ export default function BuildDetail() {
|
|||||||
icon: <IconClipboardCheck />,
|
icon: <IconClipboardCheck />,
|
||||||
content: (
|
content: (
|
||||||
<StockItemTable
|
<StockItemTable
|
||||||
|
allowAdd={false}
|
||||||
|
tableName="build-outputs"
|
||||||
params={{
|
params={{
|
||||||
build: id,
|
build: id,
|
||||||
is_building: false
|
is_building: false
|
||||||
@ -246,6 +244,8 @@ export default function BuildDetail() {
|
|||||||
icon: <IconList />,
|
icon: <IconList />,
|
||||||
content: (
|
content: (
|
||||||
<StockItemTable
|
<StockItemTable
|
||||||
|
allowAdd={false}
|
||||||
|
tableName="build-consumed"
|
||||||
params={{
|
params={{
|
||||||
consumed_by: id
|
consumed_by: id
|
||||||
}}
|
}}
|
||||||
|
@ -197,7 +197,11 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||||||
icon: <IconPackages />,
|
icon: <IconPackages />,
|
||||||
hidden: !company?.is_manufacturer && !company?.is_supplier,
|
hidden: !company?.is_manufacturer && !company?.is_supplier,
|
||||||
content: company?.pk && (
|
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 />,
|
icon: <IconPackageExport />,
|
||||||
hidden: !company?.is_customer,
|
hidden: !company?.is_customer,
|
||||||
content: company?.pk ? (
|
content: company?.pk ? (
|
||||||
<StockItemTable params={{ customer: company.pk }} />
|
<StockItemTable
|
||||||
|
allowAdd={false}
|
||||||
|
tableName="assigned-stock"
|
||||||
|
params={{ customer: company.pk }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
|
@ -486,6 +486,8 @@ export default function PartDetail() {
|
|||||||
icon: <IconPackages />,
|
icon: <IconPackages />,
|
||||||
content: part.pk && (
|
content: part.pk && (
|
||||||
<StockItemTable
|
<StockItemTable
|
||||||
|
tableName="part-stock"
|
||||||
|
allowAdd
|
||||||
params={{
|
params={{
|
||||||
part: part.pk
|
part: part.pk
|
||||||
}}
|
}}
|
||||||
|
@ -258,6 +258,7 @@ export default function PurchaseOrderDetail() {
|
|||||||
icon: <IconPackages />,
|
icon: <IconPackages />,
|
||||||
content: (
|
content: (
|
||||||
<StockItemTable
|
<StockItemTable
|
||||||
|
tableName="received-stock"
|
||||||
params={{
|
params={{
|
||||||
purchase_order: id
|
purchase_order: id
|
||||||
}}
|
}}
|
||||||
|
@ -156,6 +156,8 @@ export default function Stock() {
|
|||||||
icon: <IconPackages />,
|
icon: <IconPackages />,
|
||||||
content: (
|
content: (
|
||||||
<StockItemTable
|
<StockItemTable
|
||||||
|
tableName="location-stock"
|
||||||
|
allowAdd
|
||||||
params={{
|
params={{
|
||||||
location: id
|
location: id
|
||||||
}}
|
}}
|
||||||
|
@ -301,7 +301,10 @@ export default function StockDetail() {
|
|||||||
icon: <IconSitemap />,
|
icon: <IconSitemap />,
|
||||||
hidden: (stockitem?.child_items ?? 0) == 0,
|
hidden: (stockitem?.child_items ?? 0) == 0,
|
||||||
content: stockitem?.pk ? (
|
content: stockitem?.pk ? (
|
||||||
<StockItemTable params={{ ancestor: stockitem.pk }} />
|
<StockItemTable
|
||||||
|
tableName="child-stock"
|
||||||
|
params={{ ancestor: stockitem.pk }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
|
@ -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 {
|
return {
|
||||||
accessor: 'status',
|
accessor: accessor ?? 'status',
|
||||||
sortable: true,
|
sortable: sortable ?? true,
|
||||||
render: TableStatusRenderer(model)
|
render: TableStatusRenderer(model)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ function buildOrderTableColumns(): TableColumn[] {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
StatusColumn(ModelType.build),
|
StatusColumn({ model: ModelType.build }),
|
||||||
ProjectCodeColumn(),
|
ProjectCodeColumn(),
|
||||||
{
|
{
|
||||||
accessor: 'priority',
|
accessor: 'priority',
|
||||||
|
@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { Group, Text } from '@mantine/core';
|
import { Group, Text } from '@mantine/core';
|
||||||
import { IconCircleCheck, IconCircleX } from '@tabler/icons-react';
|
import { IconCircleCheck, IconCircleX } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
@ -11,7 +11,14 @@ import { ProgressBar } from '../../components/items/ProgressBar';
|
|||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
import {
|
||||||
|
useBuildOrderOutputFields,
|
||||||
|
useCancelBuildOutputsForm,
|
||||||
|
useCompleteBuildOutputsForm,
|
||||||
|
useScrapBuildOutputsForm
|
||||||
|
} from '../../forms/BuildForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
|
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@ -26,19 +33,21 @@ type TestResultOverview = {
|
|||||||
result: boolean;
|
result: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BuildOutputTable({
|
export default function BuildOutputTable({ build }: { build: any }) {
|
||||||
buildId,
|
|
||||||
partId
|
|
||||||
}: {
|
|
||||||
buildId: number;
|
|
||||||
partId: number;
|
|
||||||
}) {
|
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const table = useTable('build-outputs');
|
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
|
// Fetch the test templates associated with the partId
|
||||||
const { data: testTemplates } = useQuery({
|
const { data: testTemplates } = useQuery({
|
||||||
queryKey: ['buildoutputtests', partId],
|
queryKey: ['buildoutputtests', build.part],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!partId) {
|
if (!partId) {
|
||||||
return [];
|
return [];
|
||||||
@ -98,36 +107,82 @@ export default function BuildOutputTable({
|
|||||||
[partId, testTemplates]
|
[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(() => {
|
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 [
|
return [
|
||||||
<AddItemButton
|
<AddItemButton
|
||||||
tooltip={t`Add Build Output`}
|
tooltip={t`Add Build Output`}
|
||||||
hidden={!user.hasAddRole(UserRoles.build)}
|
hidden={!user.hasAddRole(UserRoles.build)}
|
||||||
|
onClick={addBuildOutput.open}
|
||||||
/>,
|
/>,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
tooltip={t`Complete selected outputs`}
|
tooltip={t`Complete selected outputs`}
|
||||||
icon={<InvenTreeIcon icon="success" />}
|
icon={<InvenTreeIcon icon="success" />}
|
||||||
color="green"
|
color="green"
|
||||||
disabled={!table.hasSelectedRecords}
|
disabled={!table.hasSelectedRecords}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOutputs(table.selectedRecords);
|
||||||
|
completeBuildOutputsForm.open();
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
tooltip={t`Scrap selected outputs`}
|
tooltip={t`Scrap selected outputs`}
|
||||||
icon={<InvenTreeIcon icon="delete" />}
|
icon={<InvenTreeIcon icon="delete" />}
|
||||||
color="red"
|
color="red"
|
||||||
disabled={!table.hasSelectedRecords}
|
disabled={!table.hasSelectedRecords}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOutputs(table.selectedRecords);
|
||||||
|
scrapBuildOutputsForm.open();
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
tooltip={t`Cancel selected outputs`}
|
tooltip={t`Cancel selected outputs`}
|
||||||
icon={<InvenTreeIcon icon="cancel" />}
|
icon={<InvenTreeIcon icon="cancel" />}
|
||||||
color="red"
|
color="red"
|
||||||
disabled={!table.hasSelectedRecords}
|
disabled={!table.hasSelectedRecords}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOutputs(table.selectedRecords);
|
||||||
|
cancelBuildOutputsForm.open();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user, partId, buildId, table.hasSelectedRecords]);
|
}, [user, table.selectedRecords, table.hasSelectedRecords]);
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any) => {
|
(record: any) => {
|
||||||
@ -148,25 +203,37 @@ export default function BuildOutputTable({
|
|||||||
title: t`Complete`,
|
title: t`Complete`,
|
||||||
tooltip: t`Complete build output`,
|
tooltip: t`Complete build output`,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: <InvenTreeIcon icon="success" />
|
icon: <InvenTreeIcon icon="success" />,
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedOutputs([record]);
|
||||||
|
completeBuildOutputsForm.open();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t`Scrap`,
|
title: t`Scrap`,
|
||||||
tooltip: t`Scrap build output`,
|
tooltip: t`Scrap build output`,
|
||||||
icon: <InvenTreeIcon icon="delete" />,
|
icon: <InvenTreeIcon icon="delete" />,
|
||||||
color: 'red'
|
color: 'red',
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedOutputs([record]);
|
||||||
|
scrapBuildOutputsForm.open();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t`Cancel`,
|
title: t`Cancel`,
|
||||||
tooltip: t`Cancel build output`,
|
tooltip: t`Cancel build output`,
|
||||||
icon: <InvenTreeIcon icon="cancel" />,
|
icon: <InvenTreeIcon icon="cancel" />,
|
||||||
color: 'red'
|
color: 'red',
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedOutputs([record]);
|
||||||
|
cancelBuildOutputsForm.open();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
},
|
},
|
||||||
[user, partId, buildId]
|
[user, partId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableColumns: TableColumn[] = useMemo(() => {
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
@ -257,6 +324,10 @@ export default function BuildOutputTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{addBuildOutput.modal}
|
||||||
|
{completeBuildOutputsForm.modal}
|
||||||
|
{scrapBuildOutputsForm.modal}
|
||||||
|
{cancelBuildOutputsForm.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
tableState={table}
|
tableState={table}
|
||||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||||
|
@ -103,7 +103,7 @@ export function PurchaseOrderTable({
|
|||||||
accessor: 'supplier_reference'
|
accessor: 'supplier_reference'
|
||||||
},
|
},
|
||||||
LineItemsProgressColumn(),
|
LineItemsProgressColumn(),
|
||||||
StatusColumn(ModelType.purchaseorder),
|
StatusColumn({ model: ModelType.purchaseorder }),
|
||||||
ProjectCodeColumn(),
|
ProjectCodeColumn(),
|
||||||
CreationDateColumn(),
|
CreationDateColumn(),
|
||||||
TargetDateColumn(),
|
TargetDateColumn(),
|
||||||
|
@ -94,7 +94,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
|
|||||||
},
|
},
|
||||||
DescriptionColumn({}),
|
DescriptionColumn({}),
|
||||||
LineItemsProgressColumn(),
|
LineItemsProgressColumn(),
|
||||||
StatusColumn(ModelType.returnorder),
|
StatusColumn({ model: ModelType.returnorder }),
|
||||||
ProjectCodeColumn(),
|
ProjectCodeColumn(),
|
||||||
CreationDateColumn(),
|
CreationDateColumn(),
|
||||||
TargetDateColumn(),
|
TargetDateColumn(),
|
||||||
|
@ -124,7 +124,7 @@ export function SalesOrderTable({
|
|||||||
},
|
},
|
||||||
DescriptionColumn({}),
|
DescriptionColumn({}),
|
||||||
LineItemsProgressColumn(),
|
LineItemsProgressColumn(),
|
||||||
StatusColumn(ModelType.salesorder),
|
StatusColumn({ model: ModelType.salesorder }),
|
||||||
ProjectCodeColumn(),
|
ProjectCodeColumn(),
|
||||||
CreationDateColumn(),
|
CreationDateColumn(),
|
||||||
TargetDateColumn(),
|
TargetDateColumn(),
|
||||||
|
@ -41,7 +41,7 @@ export default function InstalledItemsTable({
|
|||||||
accessor: 'batch',
|
accessor: 'batch',
|
||||||
switchable: false
|
switchable: false
|
||||||
},
|
},
|
||||||
StatusColumn(ModelType.stockitem)
|
StatusColumn({ model: ModelType.stockitem })
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ function stockItemTableColumns(): TableColumn[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
StatusColumn(ModelType.stockitem),
|
StatusColumn({ model: ModelType.stockitem }),
|
||||||
{
|
{
|
||||||
accessor: 'batch',
|
accessor: 'batch',
|
||||||
sortable: true
|
sortable: true
|
||||||
@ -336,12 +336,12 @@ function stockItemTableFilters(): TableFilter[] {
|
|||||||
*/
|
*/
|
||||||
export function StockItemTable({
|
export function StockItemTable({
|
||||||
params = {},
|
params = {},
|
||||||
allowAdd = true,
|
allowAdd = false,
|
||||||
tableName = 'stockitems'
|
tableName = 'stockitems'
|
||||||
}: {
|
}: {
|
||||||
params?: any;
|
params?: any;
|
||||||
allowAdd?: boolean;
|
allowAdd?: boolean;
|
||||||
tableName?: string;
|
tableName: string;
|
||||||
}) {
|
}) {
|
||||||
let tableColumns = useMemo(() => stockItemTableColumns(), []);
|
let tableColumns = useMemo(() => stockItemTableColumns(), []);
|
||||||
let tableFilters = useMemo(() => stockItemTableFilters(), []);
|
let tableFilters = useMemo(() => stockItemTableFilters(), []);
|
||||||
@ -482,7 +482,7 @@ export function StockItemTable({
|
|||||||
onClick={() => newStockItem.open()}
|
onClick={() => newStockItem.open()}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user, table]);
|
}, [user, table, allowAdd]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
Loading…
Reference in New Issue
Block a user