diff --git a/src/frontend/src/components/items/LineItemCreationMenu.tsx b/src/frontend/src/components/items/LineItemCreationMenu.tsx
new file mode 100644
index 0000000000..0b110ade02
--- /dev/null
+++ b/src/frontend/src/components/items/LineItemCreationMenu.tsx
@@ -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 (
+ }
+ hidden={hidden}
+ actions={[
+ {
+ name: addLabel,
+ icon: ,
+ onClick: onAdd
+ },
+ {
+ name: importLabel,
+ icon: ,
+ hidden: !enableImport,
+ onClick: onImport
+ }
+ ]}
+ />
+ );
+}
diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
index ce2549bdf6..ae321be63f 100644
--- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
+++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
@@ -171,6 +171,7 @@ export default function PurchaseOrderDetail() {
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 [
- {
+ 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}
{
return [
- }
- onClick={() => importLineItems.open()}
- />,
- {
+ 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()}
/>,
) {
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 [
- {
+ onAdd={() => {
newLine.open();
}}
+ onImport={() => importLineItems.open()}
/>,
];
- }, [user, editable, inProgress, orderId, table.selectedRecords]);
+ }, [
+ user,
+ editable,
+ inProgress,
+ orderId,
+ table.selectedRecords,
+ importLineItems
+ ]);
const [selectedItems, setSelectedItems] = useState([]);
@@ -294,6 +334,7 @@ export default function ReturnOrderLineItemTable({
{deleteLine.modal}
{receiveLineItems.modal}
{setOutcome.modal}
+ {importLineItems.modal}
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 [
- {
+ 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()}
/>,
{
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();
diff --git a/src/frontend/tests/pages/pui_sales.spec.ts b/src/frontend/tests/pages/pui_sales.spec.ts
index 9cc698fbe5..6dac5c5cc1 100644
--- a/src/frontend/tests/pages/pui_sales.spec.ts
+++ b/src/frontend/tests/pages/pui_sales.spec.ts
@@ -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();
diff --git a/src/frontend/tests/pui_importing.spec.ts b/src/frontend/tests/pui_importing.spec.ts
index b7d5f64061..8dfed783ef 100644
--- a/src/frontend/tests/pui_importing.spec.ts
+++ b/src/frontend/tests/pui_importing.spec.ts
@@ -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"]');