[PUI] Implement "build outputs" table (#7115)

* Update build line allocation table

- Allow display of "tracked" items in main allocation table

* Add resolveItem function for finding nested items

* Update BuildLineTable

* Allow BuildLineList to be ordered by 'trackable' field

* Bump API version

* Building out columns

* Table tweaks

* Fetch list of required test templates

* Tweaks

* Add placeholders for table actions

* Add typing

* Add placeholder buttons

* Update columns

* Add button to duplicate build order

* Add placeholder cancel action

* Refactoring

* Edit company from table

* Change "create" to "add"

* Change more from Create to Add
This commit is contained in:
Oliver 2024-04-25 10:11:44 +10:00 committed by GitHub
parent 5f54aef79a
commit d30ab932ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 581 additions and 147 deletions

View File

@ -19,9 +19,11 @@ import {
SubmitHandler, SubmitHandler,
useForm useForm
} from 'react-hook-form'; } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { api, queryClient } from '../../App'; import { api, queryClient } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { import {
NestedDict, NestedDict,
constructField, constructField,
@ -30,6 +32,7 @@ import {
mapFields mapFields
} from '../../functions/forms'; } from '../../functions/forms';
import { invalidResponse } from '../../functions/notifications'; import { invalidResponse } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import { PathParams } from '../../states/ApiState'; import { PathParams } from '../../states/ApiState';
import { import {
ApiFormField, ApiFormField,
@ -59,6 +62,8 @@ export interface ApiFormAction {
* @param successMessage : Optional message to display on successful form submission * @param successMessage : Optional message to display on successful form submission
* @param onFormSuccess : A callback function to call when the form is submitted successfully. * @param onFormSuccess : A callback function to call when the form is submitted successfully.
* @param onFormError : A callback function to call when the form is submitted with errors. * @param onFormError : A callback function to call when the form is submitted with errors.
* @param modelType : Define a model type for this form
* @param follow : Boolean, follow the result of the form (if possible)
*/ */
export interface ApiFormProps { export interface ApiFormProps {
url: ApiEndpoints | string; url: ApiEndpoints | string;
@ -79,6 +84,8 @@ export interface ApiFormProps {
successMessage?: string; successMessage?: string;
onFormSuccess?: (data: any) => void; onFormSuccess?: (data: any) => void;
onFormError?: () => void; onFormError?: () => void;
modelType?: ModelType;
follow?: boolean;
actions?: ApiFormAction[]; actions?: ApiFormAction[];
timeout?: number; timeout?: number;
} }
@ -183,6 +190,8 @@ export function ApiForm({
props: ApiFormProps; props: ApiFormProps;
optionsLoading: boolean; optionsLoading: boolean;
}) { }) {
const navigate = useNavigate();
const fields: ApiFormFieldSet = useMemo(() => { const fields: ApiFormFieldSet = useMemo(() => {
return props.fields ?? {}; return props.fields ?? {};
}, [props.fields]); }, [props.fields]);
@ -384,6 +393,12 @@ export function ApiForm({
props.onFormSuccess(response.data); props.onFormSuccess(response.data);
} }
if (props.follow) {
if (props.modelType && response.data?.pk) {
navigate(getDetailUrl(props.modelType, response.data?.pk));
}
}
// Optionally show a success message // Optionally show a success message
if (props.successMessage) { if (props.successMessage) {
notifications.hide('form-success'); notifications.hide('form-success');

View File

@ -14,8 +14,10 @@ import {
IconTrash, IconTrash,
IconUnlink IconUnlink
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { color } from '@uiw/react-codemirror';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications'; import { notYetImplemented } from '../../functions/notifications';
export type ActionDropdownItem = { export type ActionDropdownItem = {
@ -203,6 +205,24 @@ export function DeleteItemAction({
}; };
} }
export function CancelItemAction({
hidden = false,
tooltip,
onClick
}: {
hidden?: boolean;
tooltip?: string;
onClick?: () => void;
}): ActionDropdownItem {
return {
icon: <InvenTreeIcon icon="cancel" iconProps={{ color: 'red' }} />,
name: t`Cancel`,
tooltip: tooltip ?? t`Cancel`,
onClick: onClick,
hidden: hidden
};
}
// Common action button for duplicating an item // Common action button for duplicating an item
export function DuplicateItemAction({ export function DuplicateItemAction({
hidden = false, hidden = false,

View File

@ -268,8 +268,6 @@ function StockOperationsRow({
}) { }) {
const item = input.item; const item = input.item;
console.log('rec', record);
const [value, setValue] = useState<StockItemQuantity>( const [value, setValue] = useState<StockItemQuantity>(
add ? 0 : item.quantity ?? 0 add ? 0 : item.quantity ?? 0
); );

View File

@ -12,8 +12,10 @@ import {
IconCalendarStats, IconCalendarStats,
IconCategory, IconCategory,
IconCheck, IconCheck,
IconCircleCheck,
IconCircleMinus, IconCircleMinus,
IconCirclePlus, IconCirclePlus,
IconCircleX,
IconClipboardList, IconClipboardList,
IconClipboardText, IconClipboardText,
IconCopy, IconCopy,
@ -59,6 +61,7 @@ import {
IconTool, IconTool,
IconTools, IconTools,
IconTransfer, IconTransfer,
IconTransitionRight,
IconTrash, IconTrash,
IconTruck, IconTruck,
IconTruckDelivery, IconTruckDelivery,
@ -130,6 +133,10 @@ const icons = {
delete: IconTrash, delete: IconTrash,
packaging: IconPackage, packaging: IconPackage,
packages: IconPackages, packages: IconPackages,
install: IconTransitionRight,
plus: IconCirclePlus,
minus: IconCircleMinus,
cancel: IconCircleX,
// Part Icons // Part Icons
active: IconCheck, active: IconCheck,
@ -186,7 +193,8 @@ const icons = {
batch_code: IconClipboardText, batch_code: IconClipboardText,
destination: IconFlag, destination: IconFlag,
repeat_destination: IconFlagShare, repeat_destination: IconFlagShare,
unlink: IconUnlink unlink: IconUnlink,
success: IconCircleCheck
}; };
export type InvenTreeIconType = keyof typeof icons; export type InvenTreeIconType = keyof typeof icons;

View File

@ -1,5 +1,5 @@
import { randomId, useLocalStorage } from '@mantine/hooks'; import { randomId, useLocalStorage } from '@mantine/hooks';
import { useCallback, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { TableFilter } from '../tables/Filter'; import { TableFilter } from '../tables/Filter';
@ -22,6 +22,7 @@ export type TableState = {
expandedRecords: any[]; expandedRecords: any[];
setExpandedRecords: (records: any[]) => void; setExpandedRecords: (records: any[]) => void;
selectedRecords: any[]; selectedRecords: any[];
hasSelectedRecords: boolean;
setSelectedRecords: (records: any[]) => void; setSelectedRecords: (records: any[]) => void;
clearSelectedRecords: () => void; clearSelectedRecords: () => void;
hiddenColumns: string[]; hiddenColumns: string[];
@ -78,6 +79,11 @@ export function useTable(tableName: string): TableState {
setSelectedRecords([]); setSelectedRecords([]);
}, []); }, []);
const hasSelectedRecords = useMemo(
() => selectedRecords.length > 0,
[selectedRecords]
);
// Total record count // Total record count
const [recordCount, setRecordCount] = useState<number>(0); const [recordCount, setRecordCount] = useState<number>(0);
@ -126,6 +132,7 @@ export function useTable(tableName: string): TableState {
selectedRecords, selectedRecords,
setSelectedRecords, setSelectedRecords,
clearSelectedRecords, clearSelectedRecords,
hasSelectedRecords,
hiddenColumns, hiddenColumns,
setHiddenColumns, setHiddenColumns,
searchTerm, searchTerm,

View File

@ -22,6 +22,7 @@ import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
ActionDropdown, ActionDropdown,
CancelItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction,
LinkBarcodeAction, LinkBarcodeAction,
@ -36,12 +37,16 @@ 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 { useBuildOrderFields } from '../../forms/BuildForms'; import { useBuildOrderFields } from '../../forms/BuildForms';
import { useEditApiFormModal } from '../../hooks/UseForm'; import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import BuildLineTable from '../../tables/build/BuildLineTable'; import BuildLineTable from '../../tables/build/BuildLineTable';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import BuildOutputTable from '../../tables/build/BuildOutputTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable';
import { StockItemTable } from '../../tables/stock/StockItemTable'; import { StockItemTable } from '../../tables/stock/StockItemTable';
@ -213,7 +218,12 @@ 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 ? (
<BuildOutputTable buildId={build.pk} partId={build.part} />
) : (
<Skeleton />
)
// TODO: Hide if build is complete // TODO: Hide if build is complete
}, },
{ {
@ -290,6 +300,18 @@ export default function BuildDetail() {
} }
}); });
const duplicateBuild = useCreateApiFormModal({
url: ApiEndpoints.build_order_list,
title: t`Add Build Order`,
fields: buildOrderFields,
initialData: {
...build,
reference: undefined
},
follow: true,
modelType: ModelType.build
});
const buildActions = useMemo(() => { const buildActions = useMemo(() => {
// TODO: Disable certain actions based on user permissions // TODO: Disable certain actions based on user permissions
return [ return [
@ -328,7 +350,13 @@ export default function BuildDetail() {
onClick: () => editBuild.open(), onClick: () => editBuild.open(),
hidden: !user.hasChangeRole(UserRoles.build) hidden: !user.hasChangeRole(UserRoles.build)
}), }),
DuplicateItemAction({}) CancelItemAction({
tooltip: t`Cancel order`
}),
DuplicateItemAction({
onClick: () => duplicateBuild.open(),
hidden: !user.hasAddRole(UserRoles.build)
})
]} ]}
/> />
]; ];
@ -349,6 +377,7 @@ export default function BuildDetail() {
return ( return (
<> <>
{editBuild.modal} {editBuild.modal}
{duplicateBuild.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail

View File

@ -26,7 +26,10 @@ import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useManufacturerPartFields } from '../../forms/CompanyForms'; import { useManufacturerPartFields } from '../../forms/CompanyForms';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { useEditApiFormModal } from '../../hooks/UseForm'; import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -189,6 +192,17 @@ export default function ManufacturerPartDetail() {
onFormSuccess: refreshInstance onFormSuccess: refreshInstance
}); });
const duplicateManufacturerPart = useCreateApiFormModal({
url: ApiEndpoints.manufacturer_part_list,
title: t`Add Manufacturer Part`,
fields: editManufacturerPartFields,
initialData: {
...manufacturerPart
},
follow: true,
modelType: ModelType.manufacturerpart
});
const manufacturerPartActions = useMemo(() => { const manufacturerPartActions = useMemo(() => {
return [ return [
<ActionDropdown <ActionDropdown
@ -197,7 +211,8 @@ export default function ManufacturerPartDetail() {
icon={<IconDots />} icon={<IconDots />}
actions={[ actions={[
DuplicateItemAction({ DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.purchase_order) hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => duplicateManufacturerPart.open()
}), }),
EditItemAction({ EditItemAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order), hidden: !user.hasChangeRole(UserRoles.purchase_order),
@ -227,6 +242,7 @@ export default function ManufacturerPartDetail() {
return ( return (
<> <>
{editManufacturerPart.modal} {editManufacturerPart.modal}
{duplicateManufacturerPart.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail

View File

@ -8,7 +8,7 @@ import {
IconShoppingCart IconShoppingCart
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge'; import DetailsBadge from '../../components/details/DetailsBadge';
@ -26,7 +26,6 @@ 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 { useSupplierPartFields } from '../../forms/CompanyForms'; import { useSupplierPartFields } from '../../forms/CompanyForms';
import { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -43,8 +42,6 @@ export default function SupplierPartDetail() {
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const { const {
instance: supplierPart, instance: supplierPart,
instanceQuery, instanceQuery,
@ -284,11 +281,8 @@ export default function SupplierPartDetail() {
initialData: { initialData: {
...supplierPart ...supplierPart
}, },
onFormSuccess: (response: any) => { follow: true,
if (response.pk) { modelType: ModelType.supplierpart
navigate(getDetailUrl(ModelType.supplierpart, response.pk));
}
}
}); });
const breadcrumbs = useMemo(() => { const breadcrumbs = useMemo(() => {

View File

@ -24,7 +24,7 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from '@tanstack/react-query';
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -86,7 +86,6 @@ export default function PartDetail() {
const { id } = useParams(); const { id } = useParams();
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
@ -680,11 +679,8 @@ export default function PartDetail() {
initialData: { initialData: {
...part ...part
}, },
onFormSuccess: (response: any) => { follow: true,
if (response.pk) { modelType: ModelType.part
navigate(getDetailUrl(ModelType.part, response.pk));
}
}
}); });
const stockActionProps: StockOperationProps = useMemo(() => { const stockActionProps: StockOperationProps = useMemo(() => {

View File

@ -9,7 +9,7 @@ import {
IconPaperclip IconPaperclip
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
@ -17,7 +17,8 @@ import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
ActionDropdown, ActionDropdown,
BarcodeActionDropdown, BarcodeActionDropdown,
DeleteItemAction, CancelItemAction,
DuplicateItemAction,
EditItemAction, EditItemAction,
LinkBarcodeAction, LinkBarcodeAction,
UnlinkBarcodeAction, UnlinkBarcodeAction,
@ -31,7 +32,11 @@ 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 { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm'; import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -46,6 +51,7 @@ export default function PurchaseOrderDetail() {
const { id } = useParams(); const { id } = useParams();
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const { const {
instance: order, instance: order,
@ -72,6 +78,18 @@ export default function PurchaseOrderDetail() {
} }
}); });
const duplicatePurchaseOrder = useCreateApiFormModal({
url: ApiEndpoints.purchase_order_list,
title: t`Add Purchase Order`,
fields: purchaseOrderFields,
initialData: {
...order,
reference: undefined
},
follow: true,
modelType: ModelType.purchaseorder
});
const detailsPanel = useMemo(() => { const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) { if (instanceQuery.isFetching) {
return <Skeleton />; return <Skeleton />;
@ -299,8 +317,12 @@ export default function PurchaseOrderDetail() {
editPurchaseOrder.open(); editPurchaseOrder.open();
} }
}), }),
DeleteItemAction({ CancelItemAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order) tooltip: t`Cancel order`
}),
DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => duplicatePurchaseOrder.open()
}) })
]} ]}
/> />
@ -322,6 +344,7 @@ export default function PurchaseOrderDetail() {
return ( return (
<> <>
{editPurchaseOrder.modal} {editPurchaseOrder.modal}
{duplicatePurchaseOrder.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail

View File

@ -8,14 +8,16 @@ import {
IconPaperclip IconPaperclip
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
ActionDropdown, ActionDropdown,
CancelItemAction,
DeleteItemAction, DeleteItemAction,
DuplicateItemAction,
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
@ -26,7 +28,10 @@ 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 { useReturnOrderFields } from '../../forms/SalesOrderForms'; import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm'; import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -39,6 +44,7 @@ export default function ReturnOrderDetail() {
const { id } = useParams(); const { id } = useParams();
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const { const {
instance: order, instance: order,
@ -260,6 +266,18 @@ export default function ReturnOrderDetail() {
} }
}); });
const duplicateReturnOrder = useCreateApiFormModal({
url: ApiEndpoints.return_order_list,
title: t`Add Return Order`,
fields: returnOrderFields,
initialData: {
...order,
reference: undefined
},
modelType: ModelType.returnorder,
follow: true
});
const orderActions = useMemo(() => { const orderActions = useMemo(() => {
return [ return [
<ActionDropdown <ActionDropdown
@ -273,9 +291,12 @@ export default function ReturnOrderDetail() {
editReturnOrder.open(); editReturnOrder.open();
} }
}), }),
DeleteItemAction({ CancelItemAction({
hidden: !user.hasDeleteRole(UserRoles.return_order) tooltip: t`Cancel order`
// TODO: Delete? }),
DuplicateItemAction({
hidden: !user.hasChangeRole(UserRoles.return_order),
onClick: () => duplicateReturnOrder.open()
}) })
]} ]}
/> />
@ -285,6 +306,7 @@ export default function ReturnOrderDetail() {
return ( return (
<> <>
{editReturnOrder.modal} {editReturnOrder.modal}
{duplicateReturnOrder.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail

View File

@ -11,14 +11,16 @@ import {
IconTruckLoading IconTruckLoading
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
ActionDropdown, ActionDropdown,
CancelItemAction,
DeleteItemAction, DeleteItemAction,
DuplicateItemAction,
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
@ -29,7 +31,11 @@ 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 { useSalesOrderFields } from '../../forms/SalesOrderForms'; import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm'; import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -43,6 +49,7 @@ export default function SalesOrderDetail() {
const { id } = useParams(); const { id } = useParams();
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const { const {
instance: order, instance: order,
@ -212,6 +219,18 @@ export default function SalesOrderDetail() {
} }
}); });
const duplicateSalesOrder = useCreateApiFormModal({
url: ApiEndpoints.sales_order_list,
title: t`Add Sales Order`,
fields: salesOrderFields,
initialData: {
...order,
reference: undefined
},
follow: true,
modelType: ModelType.salesorder
});
const orderPanels: PanelType[] = useMemo(() => { const orderPanels: PanelType[] = useMemo(() => {
return [ return [
{ {
@ -281,13 +300,14 @@ export default function SalesOrderDetail() {
actions={[ actions={[
EditItemAction({ EditItemAction({
hidden: !user.hasChangeRole(UserRoles.sales_order), hidden: !user.hasChangeRole(UserRoles.sales_order),
onClick: () => { onClick: () => editSalesOrder.open()
editSalesOrder.open();
}
}), }),
DeleteItemAction({ CancelItemAction({
hidden: !user.hasDeleteRole(UserRoles.sales_order) tooltip: t`Cancel order`
// TODO: Delete? }),
DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.sales_order),
onClick: () => duplicateSalesOrder.open()
}) })
]} ]}
/> />
@ -309,6 +329,7 @@ export default function SalesOrderDetail() {
return ( return (
<> <>
{editSalesOrder.modal} {editSalesOrder.modal}
{duplicateSalesOrder.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail

View File

@ -1,19 +1,9 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
Alert,
Badge,
Grid,
Group,
LoadingOverlay,
Skeleton,
Stack,
Text
} from '@mantine/core';
import { import {
IconBookmark, IconBookmark,
IconBoxPadding, IconBoxPadding,
IconChecklist, IconChecklist,
IconCopy,
IconDots, IconDots,
IconHistory, IconHistory,
IconInfoCircle, IconInfoCircle,
@ -23,7 +13,7 @@ import {
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge'; import DetailsBadge from '../../components/details/DetailsBadge';
@ -74,8 +64,6 @@ export default function StockDetail() {
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
const { const {
@ -375,11 +363,8 @@ export default function StockDetail() {
initialData: { initialData: {
...stockitem ...stockitem
}, },
onFormSuccess: (response: any) => { follow: true,
if (response.pk) { modelType: ModelType.stockitem
navigate(getDetailUrl(ModelType.stockitem, response.pk));
}
}
}); });
const stockActionProps: StockOperationProps = useMemo(() => { const stockActionProps: StockOperationProps = useMemo(() => {
@ -479,6 +464,11 @@ export default function StockDetail() {
return instanceQuery.isLoading return instanceQuery.isLoading
? [] ? []
: [ : [
<DetailsBadge
color="yellow"
label={t`In Production`}
visible={stockitem.is_building}
/>,
<DetailsBadge <DetailsBadge
color="blue" color="blue"
label={t`Serial Number` + `: ${stockitem.serial}`} label={t`Serial Number` + `: ${stockitem.serial}`}

View File

@ -2,8 +2,7 @@
* Common rendering functions for table column data. * Common rendering functions for table column data.
*/ */
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Anchor } from '@mantine/core'; import { Anchor, Text } from '@mantine/core';
import { access } from 'fs';
import { YesNoButton } from '../components/buttons/YesNoButton'; import { YesNoButton } from '../components/buttons/YesNoButton';
import { Thumbnail } from '../components/images/Thumbnail'; import { Thumbnail } from '../components/images/Thumbnail';
@ -27,6 +26,34 @@ export function PartColumn(part: any, full_name?: boolean) {
); );
} }
export function LocationColumn({
accessor,
title,
sortable,
ordering
}: {
accessor: string;
title?: string;
sortable?: boolean;
ordering?: string;
}): TableColumn {
return {
accessor: accessor,
title: title ?? t`Location`,
sortable: sortable ?? true,
ordering: ordering ?? 'location',
render: (record: any) => {
let location = resolveItem(record, accessor);
if (!location) {
return <Text italic>{t`No location set`}</Text>;
}
return <Text>{location.name}</Text>;
}
};
}
export function BooleanColumn({ export function BooleanColumn({
accessor, accessor,
title, title,

View File

@ -27,7 +27,7 @@ export function TableHoverCard({
} }
return ( return (
<HoverCard withinPortal={true}> <HoverCard withinPortal={true} closeDelay={20} openDelay={250}>
<HoverCard.Target> <HoverCard.Target>
<Group spacing="xs" position="apart" noWrap={true}> <Group spacing="xs" position="apart" noWrap={true}>
{value} {value}

View File

@ -298,7 +298,7 @@ export function BomTable({
const newBomItem = useCreateApiFormModal({ const newBomItem = useCreateApiFormModal({
url: ApiEndpoints.bom_list, url: ApiEndpoints.bom_list,
title: t`Create BOM Item`, title: t`Add BOM Item`,
fields: bomItemFields(), fields: bomItemFields(),
initialData: { initialData: {
part: partId part: partId

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { PartHoverCard } from '../../components/images/Thumbnail'; import { PartHoverCard } from '../../components/images/Thumbnail';
@ -11,7 +10,6 @@ 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 { useBuildOrderFields } from '../../forms/BuildForms'; import { useBuildOrderFields } from '../../forms/BuildForms';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; 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';
@ -130,7 +128,6 @@ export function BuildOrderTable({
]; ];
}, []); }, []);
const navigate = useNavigate();
const user = useUserState(); const user = useUserState();
const table = useTable('buildorder'); const table = useTable('buildorder');
@ -146,11 +143,8 @@ export function BuildOrderTable({
sales_order: salesOrderId, sales_order: salesOrderId,
parent: parentBuildId parent: parentBuildId
}, },
onFormSuccess: (data: any) => { follow: true,
if (data.pk) { modelType: ModelType.build
navigate(getDetailUrl(ModelType.build, data.pk));
}
}
}); });
const tableActions = useMemo(() => { const tableActions = useMemo(() => {

View File

@ -0,0 +1,284 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import {
IconCircleCheck,
IconCircleX,
IconExclamationCircle
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { api } from '../../App';
import { ActionButton } from '../../components/buttons/ActionButton';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ProgressBar } from '../../components/items/ProgressBar';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { InvenTreeIcon } from '../../functions/icons';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { LocationColumn, PartColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
type TestResultOverview = {
name: string;
result: boolean;
};
export default function BuildOutputTable({
buildId,
partId
}: {
buildId: number;
partId: number;
}) {
const user = useUserState();
const table = useTable('build-outputs');
// Fetch the test templates associated with the partId
const { data: testTemplates } = useQuery({
queryKey: ['buildoutputtests', partId],
queryFn: async () => {
if (!partId) {
return [];
}
return api
.get(apiUrl(ApiEndpoints.part_test_template_list), {
params: {
part: partId,
include_inherited: true,
enabled: true,
required: true
}
})
.then((response) => response.data)
.catch(() => []);
}
});
const hasRequiredTests: boolean = useMemo(() => {
return (testTemplates?.length ?? 0) > 0;
}, [partId, testTemplates]);
// Format table records
const formatRecords = useCallback(
(records: any[]): any[] => {
records?.forEach((record: any, index: number) => {
let results: TestResultOverview[] = [];
let passCount: number = 0;
// Iterate through each
testTemplates?.forEach((template: any) => {
// Find the "newest" result for this template in the returned data
let result = record.tests
?.filter((test: any) => test.template == template.pk)
.sort((a: any, b: any) => {
return a.pk < b.pk ? 1 : -1;
})
.shift();
if (template?.required && result?.result) {
passCount += 1;
}
results.push({
name: template.test_name,
result: result?.result ?? false
});
});
records[index].passCount = passCount;
records[index].results = results;
});
return records;
},
[partId, testTemplates]
);
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)}
/>,
<ActionButton
tooltip={t`Complete selected outputs`}
icon={<InvenTreeIcon icon="success" />}
color="green"
disabled={!table.hasSelectedRecords}
/>,
<ActionButton
tooltip={t`Scrap selected outputs`}
icon={<InvenTreeIcon icon="cancel" />}
color="red"
disabled={!table.hasSelectedRecords}
/>,
<ActionButton
tooltip={t`Cancel selected outputs`}
icon={<InvenTreeIcon icon="delete" />}
color="red"
disabled={!table.hasSelectedRecords}
/>
];
}, [user, partId, buildId, table.hasSelectedRecords]);
const rowActions = useCallback(
(record: any) => {
let actions: RowAction[] = [
{
title: t`Allocate`,
tooltip: t`Allocate stock to build output`,
color: 'blue',
icon: <InvenTreeIcon icon="plus" />
},
{
title: t`Deallocate`,
tooltip: t`Deallocate stock from build output`,
color: 'red',
icon: <InvenTreeIcon icon="minus" />
},
{
title: t`Complete`,
tooltip: t`Complete build output`,
color: 'green',
icon: <InvenTreeIcon icon="success" />
},
{
title: t`Scrap`,
tooltip: t`Scrap build output`,
color: 'red',
icon: <InvenTreeIcon icon="cancel" />
},
{
title: t`Delete`,
tooltip: t`Delete build output`,
color: 'red',
icon: <InvenTreeIcon icon="delete" />
}
];
return actions;
},
[user, partId, buildId]
);
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'part',
sortable: true,
render: (record: any) => PartColumn(record?.part_detail)
},
{
accessor: 'quantity',
ordering: 'stock',
sortable: true,
switchable: false,
title: t`Build Output`,
render: (record: any) => {
let text = record.quantity;
if (record.serial) {
text = `# ${record.serial}`;
}
return (
<Group position="left" noWrap>
<Text>{text}</Text>
{record.batch && (
<Text italic size="sm">
{t`Batch`}: {record.batch}
</Text>
)}
</Group>
);
}
},
LocationColumn({
accessor: 'location_detail'
}),
{
accessor: 'allocations',
sortable: false,
switchable: false,
title: t`Allocated Items`,
render: (record: any) => {
// TODO: Implement this!
return '-';
}
},
{
accessor: 'tests',
sortable: false,
switchable: false,
title: t`Required Tests`,
hidden: !hasRequiredTests,
render: (record: any) => {
const extra =
record.results?.map((result: TestResultOverview) => {
return (
result && (
<Group position="left" key={result.name} noWrap>
{result.result ? (
<IconCircleCheck color="green" />
) : (
<IconCircleX color="red" />
)}
<Text>{result.name}</Text>
</Group>
)
);
}) ?? [];
return (
<TableHoverCard
value={
<ProgressBar
progressLabel
value={record.passCount ?? 0}
maximum={testTemplates?.length ?? 0}
/>
}
extra={extra}
title={t`Test Results`}
/>
);
}
}
];
}, [buildId, partId]);
return (
<>
<InvenTreeTable
tableState={table}
url={apiUrl(ApiEndpoints.stock_item_list)}
columns={tableColumns}
props={{
params: {
part_detail: true,
tests: true,
is_building: true,
build: buildId
},
modelType: ModelType.stockitem,
dataFormatter: formatRecords,
tableActions: tableActions,
rowActions: rowActions,
enableSelection: true
}}
/>
</>
);
}

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core'; import { Group, Text } from '@mantine/core';
import { access } from 'fs'; import { access } from 'fs';
import { useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
@ -10,13 +10,17 @@ 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 { companyFields } from '../../forms/CompanyForms'; import { companyFields } from '../../forms/CompanyForms';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import {
useCreateApiFormModal,
useEditApiFormModal
} 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';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers'; import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
import { RowEditAction } from '../RowActions';
/** /**
* A table which displays a list of company records, * A table which displays a list of company records,
@ -68,17 +72,21 @@ export function CompanyTable({
const newCompany = useCreateApiFormModal({ const newCompany = useCreateApiFormModal({
url: ApiEndpoints.company_list, url: ApiEndpoints.company_list,
title: t`New Company`, title: t`Add Company`,
fields: companyFields(), fields: companyFields(),
initialData: params, initialData: params,
onFormSuccess: (response) => { follow: true,
if (response.pk) { modelType: ModelType.company
let base = path ?? 'company'; });
navigate(`/${base}/${response.pk}`);
} else { const [selectedCompany, setSelectedCompany] = useState<number>(0);
table.refreshTable();
} const editCompany = useEditApiFormModal({
} url: ApiEndpoints.company_list,
pk: selectedCompany,
title: t`Edit Company`,
fields: companyFields(),
onFormSuccess: (record: any) => table.updateRecord(record)
}); });
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
@ -120,9 +128,27 @@ export function CompanyTable({
]; ];
}, [user]); }, [user]);
const rowActions = useCallback(
(record: any) => {
return [
RowEditAction({
hidden:
!user.hasChangeRole(UserRoles.purchase_order) &&
!user.hasChangeRole(UserRoles.sales_order),
onClick: () => {
setSelectedCompany(record.pk);
editCompany.open();
}
})
];
},
[user]
);
return ( return (
<> <>
{newCompany.modal} {newCompany.modal}
{editCompany.modal}
<InvenTreeTable <InvenTreeTable
url={apiUrl(ApiEndpoints.company_list)} url={apiUrl(ApiEndpoints.company_list)}
tableState={table} tableState={table}
@ -133,6 +159,7 @@ export function CompanyTable({
}, },
tableFilters: tableFilters, tableFilters: tableFilters,
tableActions: tableActions, tableActions: tableActions,
rowActions: rowActions,
onRowClick: (row: any) => { onRowClick: (row: any) => {
if (row.pk) { if (row.pk) {
let base = path ?? 'company'; let base = path ?? 'company';

View File

@ -491,7 +491,7 @@ export function MachineListTable({
}, [machineDrivers, createFormMachineType]); }, [machineDrivers, createFormMachineType]);
const createMachineForm = useCreateApiFormModal({ const createMachineForm = useCreateApiFormModal({
title: t`Create machine`, title: t`Add machine`,
url: ApiEndpoints.machine_list, url: ApiEndpoints.machine_list,
fields: { fields: {
name: {}, name: {},

View File

@ -278,11 +278,8 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
initialData: { initialData: {
...(props.params ?? {}) ...(props.params ?? {})
}, },
onFormSuccess: (data: any) => { follow: true,
if (data.pk) { modelType: ModelType.part
navigate(getDetailUrl(ModelType.part, data.pk));
}
}
}); });
const tableActions = useMemo(() => { const tableActions = useMemo(() => {

View File

@ -60,7 +60,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
const createManufacturerPart = useCreateApiFormModal({ const createManufacturerPart = useCreateApiFormModal({
url: ApiEndpoints.manufacturer_part_list, url: ApiEndpoints.manufacturer_part_list,
title: t`Create Manufacturer Part`, title: t`Add Manufacturer Part`,
fields: useManufacturerPartFields(), fields: useManufacturerPartFields(),
onFormSuccess: table.refreshTable, onFormSuccess: table.refreshTable,
initialData: { initialData: {

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail'; import { Thumbnail } from '../../components/images/Thumbnail';
@ -9,7 +8,6 @@ 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 { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; 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';
@ -43,8 +41,6 @@ export function PurchaseOrderTable({
supplierId?: number; supplierId?: number;
supplierPartId?: number; supplierPartId?: number;
}) { }) {
const navigate = useNavigate();
const table = useTable('purchase-order'); const table = useTable('purchase-order');
const user = useUserState(); const user = useUserState();
@ -115,13 +111,8 @@ export function PurchaseOrderTable({
initialData: { initialData: {
supplier: supplierId supplier: supplierId
}, },
onFormSuccess: (response) => { follow: true,
if (response.pk) { modelType: ModelType.purchaseorder
navigate(getDetailUrl(ModelType.purchaseorder, response.pk));
} else {
table.refreshTable();
}
}
}); });
const tableActions = useMemo(() => { const tableActions = useMemo(() => {

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail'; import { Thumbnail } from '../../components/images/Thumbnail';
@ -9,8 +8,6 @@ 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 { useReturnOrderFields } from '../../forms/SalesOrderForms'; import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; 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';
@ -37,7 +34,6 @@ import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) { export function ReturnOrderTable({ params }: { params?: any }) {
const table = useTable('return-orders'); const table = useTable('return-orders');
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
@ -101,13 +97,8 @@ export function ReturnOrderTable({ params }: { params?: any }) {
url: ApiEndpoints.return_order_list, url: ApiEndpoints.return_order_list,
title: t`Add Return Order`, title: t`Add Return Order`,
fields: returnOrderFields, fields: returnOrderFields,
onFormSuccess: (response) => { follow: true,
if (response.pk) { modelType: ModelType.returnorder
navigate(getDetailUrl(ModelType.returnorder, response.pk));
} else {
table.refreshTable();
}
}
}); });
const tableActions = useMemo(() => { const tableActions = useMemo(() => {

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail'; import { Thumbnail } from '../../components/images/Thumbnail';
@ -9,7 +8,6 @@ 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 { useSalesOrderFields } from '../../forms/SalesOrderForms'; import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; 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';
@ -43,8 +41,6 @@ export function SalesOrderTable({
const table = useTable('sales-order'); const table = useTable('sales-order');
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
@ -70,13 +66,8 @@ export function SalesOrderTable({
initialData: { initialData: {
customer: customerId customer: customerId
}, },
onFormSuccess: (response) => { follow: true,
if (response.pk) { modelType: ModelType.salesorder
navigate(getDetailUrl(ModelType.salesorder, response.pk));
} else {
table.refreshTable();
}
}
}); });
const tableActions = useMemo(() => { const tableActions = useMemo(() => {

View File

@ -240,7 +240,7 @@ export function TemplateTable({
const newTemplate = useCreateApiFormModal({ const newTemplate = useCreateApiFormModal({
url: apiEndpoint, url: apiEndpoint,
pathParams: { variant }, pathParams: { variant },
title: t`Create new` + ' ' + templateTypeTranslation, title: t`Add new` + ' ' + templateTypeTranslation,
fields: { fields: {
name: {}, name: {},
description: {}, description: {},

View File

@ -1,7 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core'; import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ActionDropdown } from '../../components/items/ActionDropdown'; import { ActionDropdown } from '../../components/items/ActionDropdown';
@ -22,14 +21,15 @@ import {
useTransferStockItem useTransferStockItem
} from '../../forms/StockForms'; } from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm'; 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';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { import {
DateColumn,
DescriptionColumn, DescriptionColumn,
LocationColumn,
PartColumn, PartColumn,
StatusColumn StatusColumn
} from '../ColumnRenderers'; } from '../ColumnRenderers';
@ -55,7 +55,7 @@ function stockItemTableColumns(): TableColumn[] {
ordering: 'stock', ordering: 'stock',
sortable: true, sortable: true,
title: t`Stock`, title: t`Stock`,
render: (record) => { render: (record: any) => {
// TODO: Push this out into a custom renderer // TODO: Push this out into a custom renderer
let quantity = record?.quantity ?? 0; let quantity = record?.quantity ?? 0;
let allocated = record?.allocated ?? 0; let allocated = record?.allocated ?? 0;
@ -198,16 +198,14 @@ function stockItemTableColumns(): TableColumn[] {
accessor: 'batch', accessor: 'batch',
sortable: true sortable: true
}, },
{ LocationColumn({
accessor: 'location', accessor: 'location_detail'
sortable: true, }),
render: function (record: any) { DateColumn({
// TODO: Custom renderer for location accessor: 'stocktake_date',
// TODO: Note, if not "In stock" we don't want to display the actual location here title: t`Stocktake`,
return record?.location_detail?.pathstring ?? record.location ?? '-'; sortable: true
} }),
},
// TODO: stocktake column
{ {
accessor: 'expiry_date', accessor: 'expiry_date',
sortable: true, sortable: true,
@ -357,8 +355,6 @@ export function StockItemTable({
const table = useTable(tableName); const table = useTable(tableName);
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const tableActionParams: StockOperationProps = useMemo(() => { const tableActionParams: StockOperationProps = useMemo(() => {
return { return {
items: table.selectedRecords, items: table.selectedRecords,
@ -377,11 +373,8 @@ export function StockItemTable({
part: params.part, part: params.part,
location: params.location location: params.location
}, },
onFormSuccess: (data: any) => { follow: true,
if (data.pk) { modelType: ModelType.stockitem
navigate(getDetailUrl(ModelType.stockitem, data.pk));
}
}
}); });
const transferStock = useTransferStockItem(tableActionParams); const transferStock = useTransferStockItem(tableActionParams);