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
|
||||
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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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[];
|
||||
};
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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/',
|
||||
|
@ -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`
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}}
|
||||
|
@ -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 />
|
||||
)
|
||||
|
@ -486,6 +486,8 @@ export default function PartDetail() {
|
||||
icon: <IconPackages />,
|
||||
content: part.pk && (
|
||||
<StockItemTable
|
||||
tableName="part-stock"
|
||||
allowAdd
|
||||
params={{
|
||||
part: part.pk
|
||||
}}
|
||||
|
@ -258,6 +258,7 @@ export default function PurchaseOrderDetail() {
|
||||
icon: <IconPackages />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
tableName="received-stock"
|
||||
params={{
|
||||
purchase_order: id
|
||||
}}
|
||||
|
@ -156,6 +156,8 @@ export default function Stock() {
|
||||
icon: <IconPackages />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
tableName="location-stock"
|
||||
allowAdd
|
||||
params={{
|
||||
location: id
|
||||
}}
|
||||
|
@ -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 />
|
||||
)
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ function buildOrderTableColumns(): TableColumn[] {
|
||||
/>
|
||||
)
|
||||
},
|
||||
StatusColumn(ModelType.build),
|
||||
StatusColumn({ model: ModelType.build }),
|
||||
ProjectCodeColumn(),
|
||||
{
|
||||
accessor: 'priority',
|
||||
|
@ -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)}
|
||||
|
@ -103,7 +103,7 @@ export function PurchaseOrderTable({
|
||||
accessor: 'supplier_reference'
|
||||
},
|
||||
LineItemsProgressColumn(),
|
||||
StatusColumn(ModelType.purchaseorder),
|
||||
StatusColumn({ model: ModelType.purchaseorder }),
|
||||
ProjectCodeColumn(),
|
||||
CreationDateColumn(),
|
||||
TargetDateColumn(),
|
||||
|
@ -94,7 +94,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
|
||||
},
|
||||
DescriptionColumn({}),
|
||||
LineItemsProgressColumn(),
|
||||
StatusColumn(ModelType.returnorder),
|
||||
StatusColumn({ model: ModelType.returnorder }),
|
||||
ProjectCodeColumn(),
|
||||
CreationDateColumn(),
|
||||
TargetDateColumn(),
|
||||
|
@ -124,7 +124,7 @@ export function SalesOrderTable({
|
||||
},
|
||||
DescriptionColumn({}),
|
||||
LineItemsProgressColumn(),
|
||||
StatusColumn(ModelType.salesorder),
|
||||
StatusColumn({ model: ModelType.salesorder }),
|
||||
ProjectCodeColumn(),
|
||||
CreationDateColumn(),
|
||||
TargetDateColumn(),
|
||||
|
@ -41,7 +41,7 @@ export default function InstalledItemsTable({
|
||||
accessor: 'batch',
|
||||
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',
|
||||
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 (
|
||||
<>
|
||||
|
Loading…
Reference in New Issue
Block a user