2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-14 06:31:27 +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:
Oliver
2025-03-18 11:35:44 +11:00
committed by GitHub
parent 947a1bcc3a
commit 8d51aa1563
122 changed files with 2434 additions and 1504 deletions

View File

@@ -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 }) => {

View 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();
});

View File

@@ -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();
});