mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	[Feature] Data export plugins (#9096)
* Move data export code out of "importer" directory * Refactoring to allow data export via plugin * Add brief docs framework * Add basic DataExportMixin class * Pass context data through to the serializer * Extract custom serializer * Refactoring * Add builtin plugin for BomExport * More refactoring * Cleanup for UseForm hooks * Allow GET methods in forms * Create new 'exporter' app * Refactor imports * Run cleanup task on boot * Add enumeration for plugin mixin types * Refactor with_mixin call * Generate export options serializer * Pass plugin information through * Offload export functionality to the plugin * Generate output * Download generated file * Refactor frontend code * Generate params for downloading * Pass custom fields through to the plugin * Implement multi-level export for BOM data * Export supplier and manufacturer information * Export substitute data * Remove old BOM exporter * Export part parameter data * Try different app order * Use GET instead of POST request - Less 'dangerous' - no chance of performing a destructive operation * Fix for constructing query parameters - Ignore any undefined values! * Trying something * Revert to POST - Required, other query data are ignored * Fix spelling mistakes * Remove SettingsMixin * Revert python version * Fix for settings.py * Fix missing return * Fix for label mixin code * Run playwright tests in --host mode * Fix for choice field - Prevent empty value if field is required * Remove debug prints * Update table header * Playwright tests for data export * Rename app from "exporter" to "data_exporter" * Add frontend table for export sessions * Updated playwright testing * Fix for unit test * Fix build order unit test * Back to using GET instead of POST - Otherwise, users need POST permissions to export! - A bit of trickery with the forms architecture * Fix remaining unit tests * Implement unit test for BOM export - Including test for custom plugin * Fix unit test * Bump API version * Enhanced playwright tests * Add debug for CI testing * Single unit test only (for debugging) * Fix typo * typo fix * Remove debugs * Docs updates * Revert typo * Update tests * Serializer fix * Fix typo * Offload data export to the background worker - Requires mocking the original request object - Will need some further unit testing! * Refactor existing models into DataOutput - Remove LabelOutput table - Remove ReportOutput table - Remove ExportOutput table - Consolidate into single API endpoint * Remove "output" tables from frontend * Refactor frontend hook to be generic * Frontend now works with background data export * Fix tasks.py * Adjust unit tests * Revert 'plugin_key' to 'plugin' * Improve user checking when printing * Updates * Remove erroneous migration file * Tweak plugin registry * Adjust playwright tests * Refactor data export - Convert into custom hook - Enable for calendar view also * Add playwright tests * Adjust unit testing * Tweak unit tests * Add extra timeout to data export * Fix for RUF045
This commit is contained in:
		| @@ -97,6 +97,11 @@ test('Build Order - Calendar', async ({ page }) => { | ||||
|   await navigate(page, 'manufacturing/index/buildorders'); | ||||
|   await activateCalendarView(page); | ||||
|  | ||||
|   // Export calendar data | ||||
|   await page.getByLabel('calendar-export-data').click(); | ||||
|   await page.getByRole('button', { name: 'Export', exact: true }).click(); | ||||
|   await page.getByText('Process completed successfully').waitFor(); | ||||
|  | ||||
|   // Check "part category" filter | ||||
|   await page.getByLabel('calendar-select-filters').click(); | ||||
|   await page.getByRole('button', { name: 'Add Filter' }).click(); | ||||
| @@ -104,6 +109,9 @@ test('Build Order - Calendar', async ({ page }) => { | ||||
|   await page.getByRole('option', { name: 'Category', exact: true }).click(); | ||||
|   await page.getByLabel('related-field-filter-category').click(); | ||||
|   await page.getByText('Part category, level 1').waitFor(); | ||||
|  | ||||
|   // Required because we downloaded a file | ||||
|   await page.context().close(); | ||||
| }); | ||||
|  | ||||
| test('Build Order - Edit', async ({ page }) => { | ||||
|   | ||||
							
								
								
									
										120
									
								
								src/frontend/tests/pui_exporting.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/frontend/tests/pui_exporting.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| import test from '@playwright/test'; | ||||
| import { globalSearch, loadTab, navigate } from './helpers'; | ||||
| import { doQuickLogin } from './login'; | ||||
|  | ||||
| // Helper function to open the export data dialog | ||||
| const openExportDialog = async (page) => { | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|   await page.getByLabel('table-export-data').click(); | ||||
|   await page.getByText('Export Format *', { exact: true }).waitFor(); | ||||
|   await page.getByText('Export Plugin *', { exact: true }).waitFor(); | ||||
| }; | ||||
|  | ||||
| // Test data export for various order types | ||||
| test('Exporting - Orders', async ({ page }) => { | ||||
|   await doQuickLogin(page, 'steven', 'wizardstaff'); | ||||
|  | ||||
|   // Download list of purchase orders | ||||
|   await navigate(page, 'purchasing/index/purchase-orders'); | ||||
|  | ||||
|   await openExportDialog(page); | ||||
|  | ||||
|   // // Select export format | ||||
|   await page.getByLabel('choice-field-export_format').click(); | ||||
|   await page.getByRole('option', { name: 'Excel' }).click(); | ||||
|  | ||||
|   // // Select export plugin (should only be one option here) | ||||
|   await page.getByLabel('choice-field-export_plugin').click(); | ||||
|   await page.getByRole('option', { name: 'InvenTree Exporter' }).click(); | ||||
|  | ||||
|   // // Export the data | ||||
|   await page.getByRole('button', { name: 'Export', exact: true }).click(); | ||||
|   await page.getByText('Process completed successfully').waitFor(); | ||||
|  | ||||
|   // Download list of purchase order items | ||||
|   await page.getByRole('cell', { name: 'PO0011' }).click(); | ||||
|   await loadTab(page, 'Line Items'); | ||||
|   await openExportDialog(page); | ||||
|   await page.getByRole('button', { name: 'Export', exact: true }).click(); | ||||
|   await page.getByText('Process completed successfully').waitFor(); | ||||
|  | ||||
|   // Download a list of build orders | ||||
|   await navigate(page, 'manufacturing/index/buildorders/'); | ||||
|   await openExportDialog(page); | ||||
|   await page.getByRole('button', { name: 'Export', exact: true }).click(); | ||||
|   await page.getByText('Process completed successfully').waitFor(); | ||||
|  | ||||
|   // Finally, navigate to the admin center and ensure the export data is available | ||||
|   await navigate(page, 'settings/admin/export/'); | ||||
|  | ||||
|   // Check for expected outputs | ||||
|   await page | ||||
|     .getByRole('link', { name: /InvenTree_Build_.*\.csv/ }) | ||||
|     .first() | ||||
|     .waitFor(); | ||||
|   await page | ||||
|     .getByRole('link', { name: /InvenTree_PurchaseOrder_.*\.xlsx/ }) | ||||
|     .first() | ||||
|     .waitFor(); | ||||
|   await page | ||||
|     .getByRole('link', { name: /InvenTree_PurchaseOrderLineItem_.*\.csv/ }) | ||||
|     .first() | ||||
|     .waitFor(); | ||||
|  | ||||
|   // Delete all exported file outputs | ||||
|   await page.getByRole('cell', { name: 'Select all records' }).click(); | ||||
|   await page.getByLabel('action-button-delete-selected').click(); | ||||
|   await page.getByRole('button', { name: 'Delete', exact: true }).click(); | ||||
|   await page.getByText('Items Deleted').waitFor(); | ||||
| }); | ||||
|  | ||||
| // Test for custom BOM exporter | ||||
| test('Exporting - BOM', async ({ page }) => { | ||||
|   await doQuickLogin(page, 'steven', 'wizardstaff'); | ||||
|  | ||||
|   await globalSearch(page, 'MAST'); | ||||
|   await page.getByLabel('search-group-results-part').locator('a').click(); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|   await loadTab(page, 'Bill of Materials'); | ||||
|   await openExportDialog(page); | ||||
|  | ||||
|   // Select export format | ||||
|   await page.getByLabel('choice-field-export_format').click(); | ||||
|   await page.getByRole('option', { name: 'TSV' }).click(); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|   // Select BOM plugin | ||||
|   await page.getByLabel('choice-field-export_plugin').click(); | ||||
|   await page.getByRole('option', { name: 'BOM Exporter' }).click(); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|   // Now, adjust the settings specific to the BOM exporter | ||||
|   await page.getByLabel('number-field-export_levels').fill('3'); | ||||
|   await page | ||||
|     .locator('label') | ||||
|     .filter({ hasText: 'Pricing DataInclude part' }) | ||||
|     .locator('span') | ||||
|     .nth(1) | ||||
|     .click(); | ||||
|   await page | ||||
|     .locator('label') | ||||
|     .filter({ hasText: 'Parameter DataInclude part' }) | ||||
|     .locator('span') | ||||
|     .nth(1) | ||||
|     .click(); | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Export', exact: true }).click(); | ||||
|   await page.getByText('Process completed successfully').waitFor(); | ||||
|  | ||||
|   // Finally, navigate to the admin center and ensure the export data is available | ||||
|   await navigate(page, 'settings/admin/export/'); | ||||
|  | ||||
|   await page.getByRole('cell', { name: 'bom-exporter' }).first().waitFor(); | ||||
|   await page | ||||
|     .getByRole('link', { name: /InvenTree_BomItem_.*\.tsv/ }) | ||||
|     .first() | ||||
|     .waitFor(); | ||||
|  | ||||
|   // Required because we downloaded a file | ||||
|   await page.context().close(); | ||||
| }); | ||||
| @@ -41,7 +41,7 @@ test('Label Printing', async ({ page }) => { | ||||
|   await page.getByRole('button', { name: 'Print', exact: true }).isEnabled(); | ||||
|   await page.getByRole('button', { name: 'Print', exact: true }).click(); | ||||
|  | ||||
|   await page.getByText('Printing completed successfully').first().waitFor(); | ||||
|   await page.getByText('Process completed successfully').first().waitFor(); | ||||
|   await page.context().close(); | ||||
| }); | ||||
|  | ||||
| @@ -77,7 +77,7 @@ test('Report Printing', async ({ page }) => { | ||||
|   await page.getByRole('button', { name: 'Print', exact: true }).isEnabled(); | ||||
|   await page.getByRole('button', { name: 'Print', exact: true }).click(); | ||||
|  | ||||
|   await page.getByText('Printing completed successfully').first().waitFor(); | ||||
|   await page.getByText('Process completed successfully').first().waitFor(); | ||||
|   await page.context().close(); | ||||
| }); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user