mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-18 23:17:41 +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
|
||||
---
|
||||
|
||||
## 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"
|
||||
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
|
||||
|
||||
|
||||
A part creation form is opened as shown below:
|
||||
New parts can be created manually by selecting the *Create Part* option from the menu. A part creation form is opened as shown below:
|
||||
|
||||
{{ 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.
|
||||
|
||||
|
||||
### 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:
|
||||
@@ -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") }}
|
||||
|
||||
## 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
|
||||
|
||||
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)
|
||||
- [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: part/index.md
|
||||
- Creating Parts: part/create.md
|
||||
- Importing Parts: part/import.md
|
||||
- Virtual Parts: part/virtual.md
|
||||
- Part Views: part/views.md
|
||||
- Tracking: part/trackable.md
|
||||
|
@@ -188,6 +188,7 @@ export function PrintingActions({
|
||||
<ActionDropdown
|
||||
tooltip={t`Printing Actions`}
|
||||
icon={<IconPrinter />}
|
||||
position='bottom-start'
|
||||
disabled={!enabled}
|
||||
actions={[
|
||||
{
|
||||
|
@@ -48,7 +48,8 @@ export function ActionDropdown({
|
||||
actions,
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
noindicator = false
|
||||
noindicator = false,
|
||||
position
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
tooltip: string;
|
||||
@@ -57,6 +58,7 @@ export function ActionDropdown({
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
noindicator?: boolean;
|
||||
position?: FloatingPosition;
|
||||
}): ReactNode {
|
||||
const hasActions = useMemo(() => {
|
||||
return actions.some((action) => !action.hidden);
|
||||
@@ -71,7 +73,7 @@ export function ActionDropdown({
|
||||
}, [tooltip]);
|
||||
|
||||
return !hidden && hasActions ? (
|
||||
<Menu position='bottom-end' key={menuName}>
|
||||
<Menu position={position ?? 'bottom-end'} key={menuName}>
|
||||
<Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}>
|
||||
<Menu.Target>
|
||||
<Tooltip
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
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 {
|
||||
type FormEventHandler,
|
||||
@@ -197,7 +197,7 @@ const SearchStep = ({
|
||||
aria-label='textbox-search-for-part'
|
||||
flex={1}
|
||||
placeholder='Search for a part'
|
||||
label={t`Search...`}
|
||||
label={t`Search`}
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.currentTarget.value)}
|
||||
/>
|
||||
@@ -224,7 +224,12 @@ const SearchStep = ({
|
||||
: t`Select supplier`
|
||||
}
|
||||
/>
|
||||
<Button disabled={!searchValue || !supplier} type='submit'>
|
||||
<Button
|
||||
color='blue'
|
||||
disabled={!searchValue || !supplier}
|
||||
type='submit'
|
||||
leftSection={<IconSearch />}
|
||||
>
|
||||
<Trans>Search</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -794,7 +799,7 @@ export default function ImportPartWizard({
|
||||
|
||||
// Create the wizard manager
|
||||
const wizard = useWizard({
|
||||
title: t`Import Part`,
|
||||
title: t`Import Supplier Part`,
|
||||
steps: [
|
||||
t`Search Supplier Part`,
|
||||
// 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 {
|
||||
type RowAction,
|
||||
RowDuplicateAction,
|
||||
@@ -17,11 +11,21 @@ import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { TableColumn } 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 ImportPartWizard from '../../components/wizards/ImportPartWizard';
|
||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import { usePartFields } from '../../forms/PartForms';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import {
|
||||
@@ -331,10 +335,12 @@ function partTableFilters(): TableFilter[] {
|
||||
* @returns
|
||||
*/
|
||||
export function PartListTable({
|
||||
enableImport = true,
|
||||
props,
|
||||
defaultPartData
|
||||
}: Readonly<{
|
||||
props: InvenTreeTableProps;
|
||||
enableImport?: boolean;
|
||||
props?: InvenTreeTableProps;
|
||||
defaultPartData?: any;
|
||||
}>) {
|
||||
const tableColumns = useMemo(() => partTableColumns(), []);
|
||||
@@ -344,9 +350,39 @@ export function PartListTable({
|
||||
const user = useUserState();
|
||||
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(() => {
|
||||
return defaultPartData ?? props.params ?? {};
|
||||
}, [defaultPartData, props.params]);
|
||||
return defaultPartData ?? props?.params ?? {};
|
||||
}, [defaultPartData, props?.params]);
|
||||
|
||||
const newPart = useCreateApiFormModal({
|
||||
url: ApiEndpoints.part_list,
|
||||
@@ -463,6 +499,7 @@ export function PartListTable({
|
||||
tooltip={t`Part Actions`}
|
||||
icon={<InvenTreeIcon icon='part' />}
|
||||
disabled={!table.hasSelectedRecords}
|
||||
position='bottom-start'
|
||||
actions={[
|
||||
{
|
||||
name: t`Set Category`,
|
||||
@@ -485,24 +522,37 @@ export function PartListTable({
|
||||
}
|
||||
]}
|
||||
/>,
|
||||
<AddItemButton
|
||||
key='add-part'
|
||||
<ActionDropdown
|
||||
key='add-parts-actions'
|
||||
tooltip={t`Add Parts`}
|
||||
position='bottom-start'
|
||||
icon={<IconPlus />}
|
||||
hidden={!user.hasAddRole(UserRoles.part)}
|
||||
tooltip={t`Add Part`}
|
||||
onClick={() => newPart.open()}
|
||||
/>,
|
||||
<ActionButton
|
||||
key='import-part'
|
||||
hidden={
|
||||
supplierPlugins.length === 0 || !user.hasAddRole(UserRoles.part)
|
||||
}
|
||||
tooltip={t`Import Part`}
|
||||
color='green'
|
||||
icon={<IconPackageImport />}
|
||||
onClick={() => importPartWizard.openWizard()}
|
||||
actions={[
|
||||
{
|
||||
name: t`Create Part`,
|
||||
icon: <IconPlus />,
|
||||
tooltip: t`Create a new part`,
|
||||
onClick: () => newPart.open()
|
||||
},
|
||||
{
|
||||
name: t`Import from File`,
|
||||
icon: <IconFileUpload />,
|
||||
tooltip: t`Import parts from a file`,
|
||||
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 (
|
||||
<>
|
||||
@@ -510,6 +560,7 @@ export function PartListTable({
|
||||
{duplicatePart.modal}
|
||||
{editPart.modal}
|
||||
{setCategory.modal}
|
||||
{importParts.modal}
|
||||
{orderPartsWizard.wizard}
|
||||
{importPartWizard.wizard}
|
||||
<InvenTreeTable
|
||||
@@ -527,12 +578,21 @@ export function PartListTable({
|
||||
enableReports: true,
|
||||
enableLabels: true,
|
||||
params: {
|
||||
...props.params,
|
||||
...props?.params,
|
||||
category_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 (
|
||||
<PartListTable
|
||||
enableImport={false}
|
||||
props={{
|
||||
enableDownload: false,
|
||||
tableFilters: tableFilters,
|
||||
|
@@ -653,6 +653,7 @@ test('Parts - Duplicate', async ({ browser }) => {
|
||||
|
||||
// Open "duplicate part" dialog
|
||||
await page.getByLabel('action-menu-part-actions').click();
|
||||
|
||||
await page.getByLabel('action-menu-part-actions-duplicate').click();
|
||||
|
||||
// Check for expected fields
|
||||
@@ -686,9 +687,22 @@ test('Parts - Import supplier part', async ({ browser }) => {
|
||||
|
||||
await page.reload();
|
||||
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
|
||||
.getByRole('textbox', { name: 'textbox-search-for-part' })
|
||||
.fill('M5');
|
||||
|
Reference in New Issue
Block a user