mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-21 08:27:38 +00:00
[UI] Part import (#10609)
* Allow import of parts from file * Extend default values for part import * Small visual tweaks * Update documentation * Update playwright test
This commit is contained in:
Binary file not shown.
Before Width: | Height: | Size: 187 KiB |
BIN
docs/docs/assets/images/part/new_parts_dropdown.png
Normal file
BIN
docs/docs/assets/images/part/new_parts_dropdown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
@@ -2,17 +2,20 @@
|
|||||||
title: Creating a Part
|
title: Creating a Part
|
||||||
---
|
---
|
||||||
|
|
||||||
## Part Creation Form
|
## Part Creation
|
||||||
|
|
||||||
New parts can be created from the *Part Category* view, by pressing the *New Part* button:
|
New parts can be created manually via the web interface, or imported from an external source.
|
||||||
|
|
||||||
|
To create or import a part, navigate to the *Parts* view in the user interface, and select the *Add Parts* dropdown menu above the parts table:
|
||||||
|
|
||||||
|
{{ image("part/new_parts_dropdown.png", "Add parts dropdown") }}
|
||||||
|
|
||||||
!!! info "Permissions"
|
!!! info "Permissions"
|
||||||
If the user does not have "create" permission for the *Part* permission group, the *New Part* button will not be available.
|
If the user does not have "create" permission for the *Part* permission group, the *Add Parts* menu will not be available.
|
||||||
|
|
||||||
{{ image("part/new_part.png", "New part") }}
|
## Create Part Form
|
||||||
|
|
||||||
|
New parts can be created manually by selecting the *Create Part* option from the menu. A part creation form is opened as shown below:
|
||||||
A part creation form is opened as shown below:
|
|
||||||
|
|
||||||
{{ image("part/part_create_form.png", "New part form") }}
|
{{ image("part/part_create_form.png", "New part form") }}
|
||||||
|
|
||||||
@@ -31,7 +34,6 @@ If this setting is enabled, the following elements are available in the form:
|
|||||||
|
|
||||||
Checking the *Create Initial Stock* form input then allows the creation of an initial quantity of stock for the new part.
|
Checking the *Create Initial Stock* form input then allows the creation of an initial quantity of stock for the new part.
|
||||||
|
|
||||||
|
|
||||||
### Supplier Options
|
### Supplier Options
|
||||||
|
|
||||||
If the part is marked as *Purchaseable*, the form provides some extra options to initialize the new part with manufacturer and / or supplier information:
|
If the part is marked as *Purchaseable*, the form provides some extra options to initialize the new part with manufacturer and / or supplier information:
|
||||||
@@ -42,9 +44,44 @@ If the *Add Supplier Data* option is checked, then supplier part and manufacture
|
|||||||
|
|
||||||
{{ image("part/part_new_suppliers.png", "Part supplier information") }}
|
{{ image("part/part_new_suppliers.png", "Part supplier information") }}
|
||||||
|
|
||||||
|
## Import from File
|
||||||
|
|
||||||
|
Parts can be imported from an external file, by selecting the *Import from File* option.
|
||||||
|
|
||||||
|
This action opens the [data import wizard](../settings/import.md), which steps the user through the process of importing parts from the selected file.
|
||||||
|
|
||||||
|
## Import from Supplier
|
||||||
|
|
||||||
|
InvenTree can integrate with external suppliers and import data from them, which helps to setup your system. Currently parts, supplier parts and manufacturer parts can be created automatically.
|
||||||
|
|
||||||
|
!!! info "Plugin Required"
|
||||||
|
To import parts from a supplier, you must install a plugin which supports that supplier.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
1. Install a supplier mixin plugin for you supplier
|
||||||
|
2. Goto "Admin Center > Plugins > [The supplier plugin]" and set the supplier company setting. Some plugins may require additional settings like API tokens.
|
||||||
|
|
||||||
|
### Import a part
|
||||||
|
|
||||||
|
New parts can be imported from the _Part Category_ view, by pressing the _Import Part_ button:
|
||||||
|
|
||||||
|
{{ image("part/import_part.png", "Import part") }}
|
||||||
|
|
||||||
|
Then just follow the wizard to confirm the category, select the parameters and create initial stock.
|
||||||
|
|
||||||
|
{{ image("part/import_part_wizard.png", "Import part wizard") }}
|
||||||
|
|
||||||
|
### Import a supplier part
|
||||||
|
|
||||||
|
If you already have the part created, you can also just import the supplier part with it's corresponding manufacturer part. Open the supplier panel for the part and use the "Import supplier part" button:
|
||||||
|
|
||||||
|
{{ image("part/import_supplier_part.png", "Import supplier part") }}
|
||||||
|
|
||||||
|
|
||||||
## Other Part Creation Methods
|
## Other Part Creation Methods
|
||||||
|
|
||||||
The following alternative methods for creating parts are supported:
|
In addition to the primary methods for creating or importing part data, the following methods are supported:
|
||||||
|
|
||||||
- [Via the REST API](../api/index.md)
|
- [Via the REST API](../api/index.md)
|
||||||
- [Using the Python library](../api/python/index.md)
|
- [Using the Python library](../api/python/index.md)
|
||||||
|
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
title: Importing Data from suppliers
|
|
||||||
---
|
|
||||||
|
|
||||||
## Import data from suppliers
|
|
||||||
|
|
||||||
InvenTree can integrate with external suppliers and import data from them, which helps to setup your system. Currently parts, supplier parts and manufacturer parts can be created automatically.
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
1. Install a supplier mixin plugin for you supplier
|
|
||||||
2. Goto "Admin Center > Plugins > [The supplier plugin]" and set the supplier company setting. Some plugins may require additional settings like API tokens.
|
|
||||||
|
|
||||||
### Import a part
|
|
||||||
|
|
||||||
New parts can be imported from the _Part Category_ view, by pressing the _Import Part_ button:
|
|
||||||
|
|
||||||
{{ image("part/import_part.png", "Import part") }}
|
|
||||||
|
|
||||||
Then just follow the wizard to confirm the category, select the parameters and create initial stock.
|
|
||||||
|
|
||||||
{{ image("part/import_part_wizard.png", "Import part wizard") }}
|
|
||||||
|
|
||||||
### Import a supplier part
|
|
||||||
|
|
||||||
If you already have the part created, you can also just import the supplier part with it's corresponding manufacturer part. Open the supplier panel for the part and use the "Import supplier part" button:
|
|
||||||
|
|
||||||
{{ image("part/import_supplier_part.png", "Import supplier part") }}
|
|
@@ -146,7 +146,6 @@ nav:
|
|||||||
- Parts:
|
- Parts:
|
||||||
- Parts: part/index.md
|
- Parts: part/index.md
|
||||||
- Creating Parts: part/create.md
|
- Creating Parts: part/create.md
|
||||||
- Importing Parts: part/import.md
|
|
||||||
- Virtual Parts: part/virtual.md
|
- Virtual Parts: part/virtual.md
|
||||||
- Part Views: part/views.md
|
- Part Views: part/views.md
|
||||||
- Tracking: part/trackable.md
|
- Tracking: part/trackable.md
|
||||||
|
@@ -188,6 +188,7 @@ export function PrintingActions({
|
|||||||
<ActionDropdown
|
<ActionDropdown
|
||||||
tooltip={t`Printing Actions`}
|
tooltip={t`Printing Actions`}
|
||||||
icon={<IconPrinter />}
|
icon={<IconPrinter />}
|
||||||
|
position='bottom-start'
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
|
@@ -48,7 +48,8 @@ export function ActionDropdown({
|
|||||||
actions,
|
actions,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
hidden = false,
|
hidden = false,
|
||||||
noindicator = false
|
noindicator = false,
|
||||||
|
position
|
||||||
}: {
|
}: {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
@@ -57,6 +58,7 @@ export function ActionDropdown({
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
noindicator?: boolean;
|
noindicator?: boolean;
|
||||||
|
position?: FloatingPosition;
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const hasActions = useMemo(() => {
|
const hasActions = useMemo(() => {
|
||||||
return actions.some((action) => !action.hidden);
|
return actions.some((action) => !action.hidden);
|
||||||
@@ -71,7 +73,7 @@ export function ActionDropdown({
|
|||||||
}, [tooltip]);
|
}, [tooltip]);
|
||||||
|
|
||||||
return !hidden && hasActions ? (
|
return !hidden && hasActions ? (
|
||||||
<Menu position='bottom-end' key={menuName}>
|
<Menu position={position ?? 'bottom-end'} key={menuName}>
|
||||||
<Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}>
|
<Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@@ -21,7 +21,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { IconArrowDown, IconPlus } from '@tabler/icons-react';
|
import { IconArrowDown, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
type FormEventHandler,
|
type FormEventHandler,
|
||||||
@@ -197,7 +197,7 @@ const SearchStep = ({
|
|||||||
aria-label='textbox-search-for-part'
|
aria-label='textbox-search-for-part'
|
||||||
flex={1}
|
flex={1}
|
||||||
placeholder='Search for a part'
|
placeholder='Search for a part'
|
||||||
label={t`Search...`}
|
label={t`Search`}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(event) => setSearchValue(event.currentTarget.value)}
|
onChange={(event) => setSearchValue(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
@@ -224,7 +224,12 @@ const SearchStep = ({
|
|||||||
: t`Select supplier`
|
: t`Select supplier`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button disabled={!searchValue || !supplier} type='submit'>
|
<Button
|
||||||
|
color='blue'
|
||||||
|
disabled={!searchValue || !supplier}
|
||||||
|
type='submit'
|
||||||
|
leftSection={<IconSearch />}
|
||||||
|
>
|
||||||
<Trans>Search</Trans>
|
<Trans>Search</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -794,7 +799,7 @@ export default function ImportPartWizard({
|
|||||||
|
|
||||||
// Create the wizard manager
|
// Create the wizard manager
|
||||||
const wizard = useWizard({
|
const wizard = useWizard({
|
||||||
title: t`Import Part`,
|
title: t`Import Supplier Part`,
|
||||||
steps: [
|
steps: [
|
||||||
t`Search Supplier Part`,
|
t`Search Supplier Part`,
|
||||||
// if partId is provided, a inventree part already exists, just import the mp/sp
|
// if partId is provided, a inventree part already exists, just import the mp/sp
|
||||||
|
@@ -1,9 +1,3 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { Group, Text } from '@mantine/core';
|
|
||||||
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { ActionButton } from '@lib/components/ActionButton';
|
|
||||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
|
||||||
import {
|
import {
|
||||||
type RowAction,
|
type RowAction,
|
||||||
RowDuplicateAction,
|
RowDuplicateAction,
|
||||||
@@ -17,11 +11,21 @@ import type { TableFilter } from '@lib/types/Filters';
|
|||||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
import type { InvenTreeTableProps } from '@lib/types/Tables';
|
import type { InvenTreeTableProps } from '@lib/types/Tables';
|
||||||
import { IconPackageImport, IconShoppingCart } from '@tabler/icons-react';
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { Group, Text } from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconFileUpload,
|
||||||
|
IconPackageImport,
|
||||||
|
IconPlus,
|
||||||
|
IconShoppingCart
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
|
import ImporterDrawer from '../../components/importer/ImporterDrawer';
|
||||||
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||||
import ImportPartWizard from '../../components/wizards/ImportPartWizard';
|
import ImportPartWizard from '../../components/wizards/ImportPartWizard';
|
||||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||||
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
||||||
|
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||||
import { usePartFields } from '../../forms/PartForms';
|
import { usePartFields } from '../../forms/PartForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import {
|
import {
|
||||||
@@ -331,10 +335,12 @@ function partTableFilters(): TableFilter[] {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function PartListTable({
|
export function PartListTable({
|
||||||
|
enableImport = true,
|
||||||
props,
|
props,
|
||||||
defaultPartData
|
defaultPartData
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
props: InvenTreeTableProps;
|
enableImport?: boolean;
|
||||||
|
props?: InvenTreeTableProps;
|
||||||
defaultPartData?: any;
|
defaultPartData?: any;
|
||||||
}>) {
|
}>) {
|
||||||
const tableColumns = useMemo(() => partTableColumns(), []);
|
const tableColumns = useMemo(() => partTableColumns(), []);
|
||||||
@@ -344,9 +350,39 @@ export function PartListTable({
|
|||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const globalSettings = useGlobalSettingsState();
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
|
const [importOpened, setImportOpened] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [selectedSession, setSelectedSession] = useState<number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const importSessionFields = useMemo(() => {
|
||||||
|
const fields = dataImporterSessionFields({
|
||||||
|
modelType: ModelType.part
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override default field values with provided fields
|
||||||
|
fields.field_defaults.value = {
|
||||||
|
...props?.params,
|
||||||
|
...defaultPartData
|
||||||
|
};
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}, [defaultPartData, props?.params]);
|
||||||
|
|
||||||
|
const importParts = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.import_session_list,
|
||||||
|
title: t`Import Parts`,
|
||||||
|
fields: importSessionFields,
|
||||||
|
onFormSuccess: (response: any) => {
|
||||||
|
setSelectedSession(response.pk);
|
||||||
|
setImportOpened(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const initialPartData = useMemo(() => {
|
const initialPartData = useMemo(() => {
|
||||||
return defaultPartData ?? props.params ?? {};
|
return defaultPartData ?? props?.params ?? {};
|
||||||
}, [defaultPartData, props.params]);
|
}, [defaultPartData, props?.params]);
|
||||||
|
|
||||||
const newPart = useCreateApiFormModal({
|
const newPart = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.part_list,
|
url: ApiEndpoints.part_list,
|
||||||
@@ -463,6 +499,7 @@ export function PartListTable({
|
|||||||
tooltip={t`Part Actions`}
|
tooltip={t`Part Actions`}
|
||||||
icon={<InvenTreeIcon icon='part' />}
|
icon={<InvenTreeIcon icon='part' />}
|
||||||
disabled={!table.hasSelectedRecords}
|
disabled={!table.hasSelectedRecords}
|
||||||
|
position='bottom-start'
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
name: t`Set Category`,
|
name: t`Set Category`,
|
||||||
@@ -485,24 +522,37 @@ export function PartListTable({
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>,
|
/>,
|
||||||
<AddItemButton
|
<ActionDropdown
|
||||||
key='add-part'
|
key='add-parts-actions'
|
||||||
|
tooltip={t`Add Parts`}
|
||||||
|
position='bottom-start'
|
||||||
|
icon={<IconPlus />}
|
||||||
hidden={!user.hasAddRole(UserRoles.part)}
|
hidden={!user.hasAddRole(UserRoles.part)}
|
||||||
tooltip={t`Add Part`}
|
actions={[
|
||||||
onClick={() => newPart.open()}
|
{
|
||||||
/>,
|
name: t`Create Part`,
|
||||||
<ActionButton
|
icon: <IconPlus />,
|
||||||
key='import-part'
|
tooltip: t`Create a new part`,
|
||||||
hidden={
|
onClick: () => newPart.open()
|
||||||
supplierPlugins.length === 0 || !user.hasAddRole(UserRoles.part)
|
},
|
||||||
}
|
{
|
||||||
tooltip={t`Import Part`}
|
name: t`Import from File`,
|
||||||
color='green'
|
icon: <IconFileUpload />,
|
||||||
icon={<IconPackageImport />}
|
tooltip: t`Import parts from a file`,
|
||||||
onClick={() => importPartWizard.openWizard()}
|
onClick: () => importParts.open(),
|
||||||
|
hidden: !enableImport
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`Import from Supplier`,
|
||||||
|
icon: <IconPackageImport />,
|
||||||
|
tooltip: t`Import parts from a supplier plugin`,
|
||||||
|
hidden: !enableImport || supplierPlugins.length === 0,
|
||||||
|
onClick: () => importPartWizard.openWizard()
|
||||||
|
}
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user, table.hasSelectedRecords, supplierPlugins]);
|
}, [user, enableImport, table.hasSelectedRecords, supplierPlugins]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -510,6 +560,7 @@ export function PartListTable({
|
|||||||
{duplicatePart.modal}
|
{duplicatePart.modal}
|
||||||
{editPart.modal}
|
{editPart.modal}
|
||||||
{setCategory.modal}
|
{setCategory.modal}
|
||||||
|
{importParts.modal}
|
||||||
{orderPartsWizard.wizard}
|
{orderPartsWizard.wizard}
|
||||||
{importPartWizard.wizard}
|
{importPartWizard.wizard}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
@@ -527,12 +578,21 @@ export function PartListTable({
|
|||||||
enableReports: true,
|
enableReports: true,
|
||||||
enableLabels: true,
|
enableLabels: true,
|
||||||
params: {
|
params: {
|
||||||
...props.params,
|
...props?.params,
|
||||||
category_detail: true,
|
category_detail: true,
|
||||||
location_detail: true
|
location_detail: true
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ImporterDrawer
|
||||||
|
sessionId={selectedSession ?? -1}
|
||||||
|
opened={selectedSession != undefined && importOpened}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedSession(undefined);
|
||||||
|
setImportOpened(false);
|
||||||
|
table.refreshTable();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PartListTable
|
<PartListTable
|
||||||
|
enableImport={false}
|
||||||
props={{
|
props={{
|
||||||
enableDownload: false,
|
enableDownload: false,
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
|
@@ -653,6 +653,7 @@ test('Parts - Duplicate', async ({ browser }) => {
|
|||||||
|
|
||||||
// Open "duplicate part" dialog
|
// Open "duplicate part" dialog
|
||||||
await page.getByLabel('action-menu-part-actions').click();
|
await page.getByLabel('action-menu-part-actions').click();
|
||||||
|
|
||||||
await page.getByLabel('action-menu-part-actions-duplicate').click();
|
await page.getByLabel('action-menu-part-actions-duplicate').click();
|
||||||
|
|
||||||
// Check for expected fields
|
// Check for expected fields
|
||||||
@@ -686,9 +687,22 @@ test('Parts - Import supplier part', async ({ browser }) => {
|
|||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'action-button-import-part' }).click();
|
// Open "Add parts" menu
|
||||||
|
await page.getByRole('button', { name: 'action-menu-add-parts' }).click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'action-menu-add-parts-create-part' })
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'action-menu-add-parts-import-from-file' })
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', {
|
||||||
|
name: 'action-menu-add-parts-import-from-supplier'
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('textbox', { name: 'textbox-search-for-part' })
|
.getByRole('textbox', { name: 'textbox-search-for-part' })
|
||||||
.fill('M5');
|
.fill('M5');
|
||||||
|
Reference in New Issue
Block a user