2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-09 15:10:54 +00:00

[bug] Data import fix (#9962)

* Permission fix for data importer endpoint

* Add playwright tests

* Bump API version
This commit is contained in:
Oliver
2025-07-06 18:22:37 +10:00
committed by GitHub
parent 945cb46f32
commit 042039754b
7 changed files with 107 additions and 12 deletions

View File

@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 363
INVENTREE_API_VERSION = 364
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v364 -> 2025-07-06 : https://github.com/inventree/InvenTree/pull/9962
- Fix permissions for the DataImportSession API endpoints
v363 -> 2025-07-04 : https://github.com/inventree/InvenTree/pull/9954
- Adds "user_detail" field to the ApiToken serializer

View File

@ -30,7 +30,7 @@ class DataImporterPermission(permissions.BasePermission):
def has_permission(self, request, view):
"""Class level permission checks are handled via InvenTree.permissions.IsAuthenticatedOrReadScope."""
return True
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
"""Check if the user has permission to access the imported object."""
@ -91,25 +91,25 @@ class DataImporterModelList(APIView):
return Response(models)
class DataImportSessionList(DataImporterPermission, BulkDeleteMixin, ListCreateAPI):
"""API endpoint for accessing a list of DataImportSession objects."""
class DataImportSessionMixin:
"""Mixin class for DataImportSession API views."""
queryset = importer.models.DataImportSession.objects.all()
serializer_class = importer.serializers.DataImportSessionSerializer
permission_classes = [DataImporterPermission]
class DataImportSessionList(BulkDeleteMixin, DataImportSessionMixin, ListCreateAPI):
"""API endpoint for accessing a list of DataImportSession objects."""
filter_backends = SEARCH_ORDER_FILTER
filterset_fields = ['model_type', 'status', 'user']
ordering_fields = ['timestamp', 'status', 'model_type']
class DataImportSessionDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
class DataImportSessionDetail(DataImportSessionMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for a single DataImportSession object."""
queryset = importer.models.DataImportSession.objects.all()
serializer_class = importer.serializers.DataImportSessionSerializer
class DataImportSessionAcceptFields(APIView):
"""API endpoint to accept the field mapping for a DataImportSession."""

View File

@ -179,7 +179,7 @@ export function ApiFormField({
radius='lg'
size='sm'
error={definition.error ?? error?.message}
onChange={(event) => onChange(event.currentTarget.checked)}
onChange={(event: any) => onChange(event.currentTarget.checked)}
/>
);
case 'date':
@ -217,6 +217,7 @@ export function ApiFormField({
return (
<FileInput
{...reducedDefinition}
aria-label={`file-field-${fieldName}`}
id={fieldId}
ref={field.ref}
radius='sm'

View File

@ -0,0 +1,5 @@
Assembly,Component,Reference,Quantity,Overage,Allow Variants,Gets inherited,Optional,Consumable,Note,ID,Pricing min,Pricing max,Pricing min total,Pricing max total,Pricing updated,Component.Ipn,Component.Name,Component.Description,Validated,Available Stock,Available substitute stock,Available variant stock,External stock,On Order,In Production,Can Build
87,66,Screws,4.0,,False,False,False,False,,16,0.28,0.648622,1.12,2.594488,2024-08-08 06:55,,M3x8 Torx,"Torx head screw, M3 thread, 8.0mm",True,485.0,0.0,0.0,0.0,0.0,0.0,121.25
87,67,Large screw,1.0,,False,False,False,False,,17,0.574802,0.574802,0.574802,0.574802,2024-07-27 05:13,,M3x10 Torx,"Torx head screw, M3 thread, 10.0mm",True,1450.0,0.0,0.0,0.0,0.0,0.0,1450.0
87,82,Enclosure,1.0,,False,False,False,False,,15,,,,,2024-07-27 05:08,,1551ABK,"Small plastic enclosure, black",True,165.0,223.0,0.0,0.0,0.0,0.0,388.0
87,88,PCBA,1.0,,True,False,False,False,Assembled board,23,80.431083,129.328176,80.431083,129.328176,2024-12-27 23:14,002.01-PCBA,Widget Board (assembled),Assembled PCB for converting electricity into magic smoke,True,55.0,0.0,0.0,0.0,0.0,0.0,55.0
1 Assembly Component Reference Quantity Overage Allow Variants Gets inherited Optional Consumable Note ID Pricing min Pricing max Pricing min total Pricing max total Pricing updated Component.Ipn Component.Name Component.Description Validated Available Stock Available substitute stock Available variant stock External stock On Order In Production Can Build
2 87 66 Screws 4.0 False False False False 16 0.28 0.648622 1.12 2.594488 2024-08-08 06:55 M3x8 Torx Torx head screw, M3 thread, 8.0mm True 485.0 0.0 0.0 0.0 0.0 0.0 121.25
3 87 67 Large screw 1.0 False False False False 17 0.574802 0.574802 0.574802 0.574802 2024-07-27 05:13 M3x10 Torx Torx head screw, M3 thread, 10.0mm True 1450.0 0.0 0.0 0.0 0.0 0.0 1450.0
4 87 82 Enclosure 1.0 False False False False 15 2024-07-27 05:08 1551ABK Small plastic enclosure, black True 165.0 223.0 0.0 0.0 0.0 0.0 388.0
5 87 88 PCBA 1.0 True False False False Assembled board 23 80.431083 129.328176 80.431083 129.328176 2024-12-27 23:14 002.01-PCBA Widget Board (assembled) Assembled PCB for converting electricity into magic smoke True 55.0 0.0 0.0 0.0 0.0 0.0 55.0

View File

@ -0,0 +1,4 @@
ID,Supplier Part,Quantity,Reference,Notes,Order,Build Order,Overdue,Received,Purchase price,Currency,Auto Pricing,Destination,Target Date,Total price,Link,SKU,MPN,Internal Part Number,Internal Part,Internal Part Name
27,1034,100.0,,,11,,False,100.0,2.5,USD,True,,,250.0,,WE-10AWG-WT,,,897,Silicon Wire 10AWG White
28,1033,10.0,,,11,,False,10.0,135.0,USD,True,,,1350.0,,WE-12AWG-BK-1000,,,899,Silicon Wire 12AWG Black
29,1028,123.0,,,11,,False,123.0,1.0,USD,True,,,123.0,,WC-12AWG-WT,,,901,Silicon Wire 12AWG White
1 ID Supplier Part Quantity Reference Notes Order Build Order Overdue Received Purchase price Currency Auto Pricing Destination Target Date Total price Link SKU MPN Internal Part Number Internal Part Internal Part Name
2 27 1034 100.0 11 False 100.0 2.5 USD True 250.0 WE-10AWG-WT 897 Silicon Wire 10AWG White
3 28 1033 10.0 11 False 10.0 135.0 USD True 1350.0 WE-12AWG-BK-1000 899 Silicon Wire 12AWG Black
4 29 1028 123.0 11 False 123.0 1.0 USD True 123.0 WC-12AWG-WT 901 Silicon Wire 12AWG White

View File

@ -114,7 +114,10 @@ export const doCachedLogin = async (
await page.context().storageState({ path: fn });
if (url) {
await navigate(page, url, { baseUrl: options?.baseUrl });
await navigate(page, url, {
baseUrl: options?.baseUrl,
waitUntil: 'networkidle'
});
}
return page;

View File

@ -0,0 +1,79 @@
import test from '@playwright/test';
import { doCachedLogin } from './login';
test('Importing - Admin Center', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
url: 'settings/admin/import'
});
await page
.getByRole('button', { name: 'action-button-create-import-' })
.click();
const fileInput = await page.locator('input[type="file"]');
await fileInput.setInputFiles('./tests/fixtures/bom_data.csv');
await page.getByRole('button', { name: 'Submit' }).click();
// Submitting without selecting model type, should show error
await page.getByText('This field is required.').waitFor();
await page.getByText('Errors exist for one or more').waitFor();
await page
.getByRole('textbox', { name: 'choice-field-model_type' })
.fill('Cat');
await page
.getByRole('option', { name: 'Part Category', exact: true })
.click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Description (optional)').waitFor();
await page.getByText('Parent Category').waitFor();
});
test('Importing - BOM', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
url: 'part/87/bom'
});
await page
.getByRole('button', { name: 'action-button-import-bom-data' })
.click();
// Select BOM file fixture for import
const fileInput = await page.locator('input[type="file"]');
await fileInput.setInputFiles('./tests/fixtures/bom_data.csv');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Mapping data columns to database fields').waitFor();
await page.getByRole('button', { name: 'Accept Column Mapping' }).click();
await page.getByText('Processing Data').waitFor();
await page.getByText('0 / 4').waitFor();
await page
.getByLabel('Importing DataUpload FileMap')
.getByText('002.01-PCBA | Widget Board')
.waitFor();
});
test('Importing - Purchase Order', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
url: 'purchasing/purchase-order/15/line-items'
});
await page
.getByRole('button', { name: 'action-button-import-line-' })
.click();
const fileInput = await page.locator('input[type="file"]');
await fileInput.setInputFiles('./tests/fixtures/po_data.csv');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('cell', { name: 'Database Field' }).waitFor();
await page.getByRole('cell', { name: 'Field Description' }).waitFor();
});