mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-30 00:46:40 +00:00
[UI] Data import for order lines (#12692)
* [UI] Data import for order lines * Adjust playwright tests
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { IconFileUpload, IconPlus } from '@tabler/icons-react';
|
||||
|
||||
import { ActionDropdown } from './ActionDropdown';
|
||||
|
||||
/**
|
||||
* A split "add" button which allows either direct creation of a single line item,
|
||||
* or bulk creation via a data import session.
|
||||
*/
|
||||
export function LineItemCreationMenu({
|
||||
tooltip,
|
||||
addLabel,
|
||||
importLabel,
|
||||
hidden = false,
|
||||
enableImport = true,
|
||||
onAdd,
|
||||
onImport
|
||||
}: Readonly<{
|
||||
tooltip: string;
|
||||
addLabel: string;
|
||||
importLabel: string;
|
||||
hidden?: boolean;
|
||||
enableImport?: boolean;
|
||||
onAdd: () => void;
|
||||
onImport: () => void;
|
||||
}>) {
|
||||
return (
|
||||
<ActionDropdown
|
||||
key='add-line-item-actions'
|
||||
tooltip={tooltip}
|
||||
position='bottom-start'
|
||||
icon={<IconPlus />}
|
||||
hidden={hidden}
|
||||
actions={[
|
||||
{
|
||||
name: addLabel,
|
||||
icon: <IconPlus />,
|
||||
onClick: onAdd
|
||||
},
|
||||
{
|
||||
name: importLabel,
|
||||
icon: <IconFileUpload />,
|
||||
hidden: !enableImport,
|
||||
onClick: onImport
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -171,6 +171,7 @@ export default function PurchaseOrderDetail() {
|
||||
<Accordion.Panel>
|
||||
<ExtraLineItemTable
|
||||
endpoint={ApiEndpoints.purchase_order_extra_line_list}
|
||||
importModelType='purchaseorderextraline'
|
||||
orderId={order.pk}
|
||||
orderDetailRefresh={refreshInstance}
|
||||
currency={orderCurrency}
|
||||
|
||||
@@ -136,6 +136,7 @@ export default function ReturnOrderDetail() {
|
||||
<Accordion.Panel>
|
||||
<ExtraLineItemTable
|
||||
endpoint={ApiEndpoints.return_order_extra_line_list}
|
||||
importModelType='returnorderextraline'
|
||||
orderId={order.pk}
|
||||
orderDetailRefresh={refreshInstance}
|
||||
currency={orderCurrency}
|
||||
|
||||
@@ -173,6 +173,7 @@ export default function SalesOrderDetail() {
|
||||
<Accordion.Panel>
|
||||
<ExtraLineItemTable
|
||||
endpoint={ApiEndpoints.sales_order_extra_line_list}
|
||||
importModelType='salesorderextraline'
|
||||
orderId={order.pk}
|
||||
editable={lineItemsEditable}
|
||||
orderDetailRefresh={refreshInstance}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import {
|
||||
type RowAction,
|
||||
RowDeleteAction,
|
||||
RowDuplicateAction,
|
||||
RowEditAction
|
||||
} from '@lib/components/RowActions';
|
||||
import type { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import type { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import useTable from '@lib/hooks/UseTable';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { LineItemCreationMenu } from '../../components/items/LineItemCreationMenu';
|
||||
import {
|
||||
DecimalColumn,
|
||||
DescriptionColumn,
|
||||
@@ -25,15 +26,18 @@ import {
|
||||
import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { extraLineItemFields } from '../../forms/CommonForms';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useImporterState } from '../../states/ImporterState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
|
||||
export default function ExtraLineItemTable({
|
||||
endpoint,
|
||||
importModelType,
|
||||
orderId,
|
||||
orderDetailRefresh,
|
||||
currency,
|
||||
@@ -41,6 +45,7 @@ export default function ExtraLineItemTable({
|
||||
role
|
||||
}: Readonly<{
|
||||
endpoint: ApiEndpoints;
|
||||
importModelType: ModelType | string;
|
||||
orderId: number;
|
||||
editable: boolean;
|
||||
orderDetailRefresh: () => void;
|
||||
@@ -49,6 +54,7 @@ export default function ExtraLineItemTable({
|
||||
}>) {
|
||||
const table = useTable('extra-line-item');
|
||||
const user = useUserState();
|
||||
const openImporter = useImporterState((state) => state.openImporter);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
@@ -126,6 +132,31 @@ export default function ExtraLineItemTable({
|
||||
table: table
|
||||
});
|
||||
|
||||
const importSessionFields = useMemo(() => {
|
||||
const fields = dataImporterSessionFields({ modelType: importModelType });
|
||||
|
||||
fields.field_overrides.value = {
|
||||
order: orderId
|
||||
};
|
||||
|
||||
fields.field_defaults.value = {
|
||||
price_currency: currency
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [orderId, currency, importModelType]);
|
||||
|
||||
const importLineItems = useCreateApiFormModal({
|
||||
url: ApiEndpoints.import_session_list,
|
||||
title: t`Import Line Items`,
|
||||
fields: importSessionFields,
|
||||
onFormSuccess: (response: any) => {
|
||||
openImporter(response.pk, {
|
||||
onClose: table.refreshTable
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
return [
|
||||
@@ -157,25 +188,29 @@ export default function ExtraLineItemTable({
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
key='add-line-item'
|
||||
<LineItemCreationMenu
|
||||
key='add-line-item-actions'
|
||||
tooltip={t`Add Extra Line Item`}
|
||||
addLabel={t`Add Extra Line Item`}
|
||||
importLabel={t`Import Line Items`}
|
||||
hidden={!editable || !user.hasAddRole(role)}
|
||||
onClick={() => {
|
||||
onAdd={() => {
|
||||
setInitialData({
|
||||
order: orderId
|
||||
});
|
||||
newLineItem.open();
|
||||
}}
|
||||
onImport={() => importLineItems.open()}
|
||||
/>
|
||||
];
|
||||
}, [editable, user, role]);
|
||||
}, [editable, user, role, orderId, importLineItems]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{newLineItem.modal}
|
||||
{editLineItem.modal}
|
||||
{deleteLineItem.modal}
|
||||
{importLineItems.modal}
|
||||
<InvenTreeTable
|
||||
tableState={table}
|
||||
url={apiUrl(endpoint)}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Text } from '@mantine/core';
|
||||
import { IconFileArrowLeft, IconSquareArrowRight } from '@tabler/icons-react';
|
||||
import { IconSquareArrowRight } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { ActionButton } from '@lib/components/ActionButton';
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import { ProgressBar } from '@lib/components/ProgressBar';
|
||||
import {
|
||||
type RowAction,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
} from '../../components/tables/ColumnRenderers';
|
||||
import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
|
||||
|
||||
import { LineItemCreationMenu } from '../../components/items/LineItemCreationMenu';
|
||||
import { AppRowViewAction } from '../../components/tables/AppRowActions';
|
||||
import { TableHoverCard } from '../../components/tables/TableHoverCard';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
@@ -397,23 +397,19 @@ export function PurchaseOrderLineItemTable({
|
||||
// Custom table actions
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<ActionButton
|
||||
key='import-line-items'
|
||||
hidden={!editable || !user.hasAddRole(UserRoles.purchase_order)}
|
||||
tooltip={t`Import Line Items`}
|
||||
icon={<IconFileArrowLeft />}
|
||||
onClick={() => importLineItems.open()}
|
||||
/>,
|
||||
<AddItemButton
|
||||
key='add-line-item'
|
||||
<LineItemCreationMenu
|
||||
key='add-line-item-actions'
|
||||
tooltip={t`Add Line Item`}
|
||||
onClick={() => {
|
||||
addLabel={t`Add Line Item`}
|
||||
importLabel={t`Import Line Items`}
|
||||
hidden={!editable || !user.hasAddRole(UserRoles.purchase_order)}
|
||||
onAdd={() => {
|
||||
setInitialData({
|
||||
order: orderId
|
||||
});
|
||||
newLine.open();
|
||||
}}
|
||||
hidden={!editable || !user?.hasAddRole(UserRoles.purchase_order)}
|
||||
onImport={() => importLineItems.open()}
|
||||
/>,
|
||||
<ActionButton
|
||||
key='receive-items'
|
||||
|
||||
@@ -3,7 +3,6 @@ import { IconFlag, IconSquareArrowRight } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { ActionButton } from '@lib/components/ActionButton';
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import {
|
||||
type RowAction,
|
||||
RowDeleteAction,
|
||||
@@ -17,6 +16,7 @@ import useTable from '@lib/hooks/UseTable';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { Alert } from '@mantine/core';
|
||||
import { LineItemCreationMenu } from '../../components/items/LineItemCreationMenu';
|
||||
import {
|
||||
DateColumn,
|
||||
DescriptionColumn,
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { StatusFilterOptions } from '../../components/tables/Filter';
|
||||
import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import {
|
||||
useReceiveReturnOrderLineItems,
|
||||
useReturnOrderLineItemFields
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { useImporterState } from '../../states/ImporterState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
|
||||
export default function ReturnOrderLineItemTable({
|
||||
@@ -63,6 +65,7 @@ export default function ReturnOrderLineItemTable({
|
||||
}>) {
|
||||
const table = useTable('return-order-line-item');
|
||||
const user = useUserState();
|
||||
const openImporter = useImporterState((state) => state.openImporter);
|
||||
|
||||
const roStatus = useStatusCodes({ modelType: ModelType.returnorder });
|
||||
|
||||
@@ -112,6 +115,33 @@ export default function ReturnOrderLineItemTable({
|
||||
table: table
|
||||
});
|
||||
|
||||
const importSessionFields = useMemo(() => {
|
||||
const fields = dataImporterSessionFields({
|
||||
modelType: ModelType.returnorderlineitem
|
||||
});
|
||||
|
||||
fields.field_overrides.value = {
|
||||
order: orderId
|
||||
};
|
||||
|
||||
fields.field_defaults.value = {
|
||||
price_currency: currency
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [orderId, currency]);
|
||||
|
||||
const importLineItems = useCreateApiFormModal({
|
||||
url: ApiEndpoints.import_session_list,
|
||||
title: t`Import Line Items`,
|
||||
fields: importSessionFields,
|
||||
onFormSuccess: (response: any) => {
|
||||
openImporter(response.pk, {
|
||||
onClose: table.refreshTable
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const setOutcome = useBulkEditApiFormModal({
|
||||
url: ApiEndpoints.return_order_line_list,
|
||||
items: table.selectedIds,
|
||||
@@ -204,13 +234,16 @@ export default function ReturnOrderLineItemTable({
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
key='add-line-item'
|
||||
<LineItemCreationMenu
|
||||
key='add-line-item-actions'
|
||||
tooltip={t`Add Line Item`}
|
||||
addLabel={t`Add Line Item`}
|
||||
importLabel={t`Import Line Items`}
|
||||
hidden={!editable || !user.hasAddRole(UserRoles.return_order)}
|
||||
onClick={() => {
|
||||
onAdd={() => {
|
||||
newLine.open();
|
||||
}}
|
||||
onImport={() => importLineItems.open()}
|
||||
/>,
|
||||
<ActionButton
|
||||
key='receive-items'
|
||||
@@ -240,7 +273,14 @@ export default function ReturnOrderLineItemTable({
|
||||
disabled={table.selectedRecords.length == 0}
|
||||
/>
|
||||
];
|
||||
}, [user, editable, inProgress, orderId, table.selectedRecords]);
|
||||
}, [
|
||||
user,
|
||||
editable,
|
||||
inProgress,
|
||||
orderId,
|
||||
table.selectedRecords,
|
||||
importLineItems
|
||||
]);
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<any[]>([]);
|
||||
|
||||
@@ -294,6 +334,7 @@ export default function ReturnOrderLineItemTable({
|
||||
{deleteLine.modal}
|
||||
{receiveLineItems.modal}
|
||||
{setOutcome.modal}
|
||||
{importLineItems.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.return_order_line_list)}
|
||||
tableState={table}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { ActionButton } from '@lib/components/ActionButton';
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import { ProgressBar } from '@lib/components/ProgressBar';
|
||||
import {
|
||||
type RowAction,
|
||||
@@ -44,12 +43,14 @@ import {
|
||||
} from '../../components/tables/ColumnRenderers';
|
||||
import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
|
||||
|
||||
import { LineItemCreationMenu } from '../../components/items/LineItemCreationMenu';
|
||||
import { AppRowViewAction } from '../../components/tables/AppRowActions';
|
||||
import RowExpansionIcon from '../../components/tables/RowExpansionIcon';
|
||||
import { TableHoverCard } from '../../components/tables/TableHoverCard';
|
||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||
import { formatCurrency, formatDecimal } from '../../defaults/formatters';
|
||||
import { useBuildOrderFields } from '../../forms/BuildForms';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import {
|
||||
useAllocateToSalesOrderForm,
|
||||
useSalesOrderAllocateSerialsFields,
|
||||
@@ -62,6 +63,7 @@ import {
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useImporterState } from '../../states/ImporterState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import SalesOrderAllocationTable from './SalesOrderAllocationTable';
|
||||
|
||||
@@ -81,6 +83,34 @@ export default function SalesOrderLineItemTable({
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
const table = useTable('sales-order-line-item');
|
||||
const openImporter = useImporterState((state) => state.openImporter);
|
||||
|
||||
const importSessionFields = useMemo(() => {
|
||||
const fields = dataImporterSessionFields({
|
||||
modelType: 'salesorderlineitem'
|
||||
});
|
||||
|
||||
fields.field_overrides.value = {
|
||||
order: orderId
|
||||
};
|
||||
|
||||
fields.field_defaults.value = {
|
||||
sale_price_currency: currency
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [orderId, currency]);
|
||||
|
||||
const importLineItems = useCreateApiFormModal({
|
||||
url: ApiEndpoints.import_session_list,
|
||||
title: t`Import Line Items`,
|
||||
fields: importSessionFields,
|
||||
onFormSuccess: (response: any) => {
|
||||
openImporter(response.pk, {
|
||||
onClose: table.refreshTable
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
@@ -408,16 +438,19 @@ export default function SalesOrderLineItemTable({
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
key='add-line-item'
|
||||
<LineItemCreationMenu
|
||||
key='add-line-item-actions'
|
||||
tooltip={t`Add Line Item`}
|
||||
onClick={() => {
|
||||
addLabel={t`Add Line Item`}
|
||||
importLabel={t`Import Line Items`}
|
||||
hidden={!editable || !user.hasAddRole(UserRoles.sales_order)}
|
||||
onAdd={() => {
|
||||
setInitialData({
|
||||
order: orderId
|
||||
});
|
||||
newLine.open();
|
||||
}}
|
||||
hidden={!editable || !user.hasAddRole(UserRoles.sales_order)}
|
||||
onImport={() => importLineItems.open()}
|
||||
/>,
|
||||
<ActionButton
|
||||
key='order-parts'
|
||||
@@ -463,7 +496,8 @@ export default function SalesOrderLineItemTable({
|
||||
user,
|
||||
orderId,
|
||||
table.hasSelectedRecords,
|
||||
table.selectedRecords
|
||||
table.selectedRecords,
|
||||
importLineItems
|
||||
]);
|
||||
|
||||
const rowActions = useCallback(
|
||||
@@ -603,6 +637,7 @@ export default function SalesOrderLineItemTable({
|
||||
{editLine.modal}
|
||||
{deleteLine.modal}
|
||||
{newLine.modal}
|
||||
{importLineItems.modal}
|
||||
{newBuildOrder.modal}
|
||||
{allocateBySerials.modal}
|
||||
{allocateStock.modal}
|
||||
|
||||
@@ -379,9 +379,11 @@ test('Purchase Orders - Price Breaks', async ({ browser }) => {
|
||||
url: 'purchasing/purchase-order/14/line-items'
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'action-menu-add-line-item' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-add-line-item' })
|
||||
.getByRole('menuitem', { name: 'action-menu-add-line-item-add' })
|
||||
.click();
|
||||
|
||||
await page.getByLabel('related-field-part').fill('002.01');
|
||||
await page.getByRole('option', { name: 'PCBWOY PCB-002.01' }).click();
|
||||
|
||||
|
||||
@@ -435,9 +435,11 @@ test('Sales Orders - Price Breaks', async ({ browser }) => {
|
||||
url: 'sales/sales-order/14/line-items'
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'action-menu-add-line-item' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-add-line-item' })
|
||||
.getByRole('menuitem', { name: 'action-menu-add-line-item-add' })
|
||||
.click();
|
||||
|
||||
await page.getByLabel('related-field-part').fill('software');
|
||||
await page.getByRole('option', { name: 'Software License' }).click();
|
||||
|
||||
|
||||
@@ -162,8 +162,11 @@ test('Importing - Purchase Order', async ({ browser }) => {
|
||||
url: 'purchasing/purchase-order/15/line-items'
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'action-menu-add-line-item' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-import-line-' })
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-line-item-import-line-items'
|
||||
})
|
||||
.click();
|
||||
|
||||
const fileInput = await page.locator('input[type="file"]');
|
||||
@@ -181,8 +184,11 @@ test('Importing - Natural Keys', async ({ browser }) => {
|
||||
});
|
||||
|
||||
// Import line item data, but use natural keys as the import fields
|
||||
await page.getByRole('button', { name: 'action-menu-add-line-item' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-import-line-' })
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-line-item-import-line-items'
|
||||
})
|
||||
.click();
|
||||
|
||||
const fileInput = await page.locator('input[type="file"]');
|
||||
|
||||
Reference in New Issue
Block a user