2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-11-14 03:46:44 +00:00

Supplier Mixin (#9761)

* commit initial draft for supplier import

* complete import wizard

* allow importing only mp and sp

* improved sample supplier plugin

* add docs

* add tests

* bump api version

* fix schema docu

* fix issues from code review

* commit unstaged changes

* fix test

* refactor part parameter bulk creation

* try to fix test

* fix tests

* fix test for mysql

* fix test

* support multiple suppliers by a single plugin

* hide import button if there is no supplier import plugin

* make form submitable via enter

* add pui test

* try to prevent race condition

* refactor api calls in pui tests

* try to fix tests again?

* fix tests

* trigger: ci

* update changelog

* fix api_version

* fix style

* Update CHANGELOG.md

Co-authored-by: Matthias Mair <code@mjmair.com>

* add user docs

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Lukas Wolf
2025-10-17 22:13:03 +02:00
committed by GitHub
parent d534f67c62
commit de270a5fe7
41 changed files with 2298 additions and 119 deletions

10
src/frontend/tests/api.ts Normal file
View File

@@ -0,0 +1,10 @@
import { request } from '@playwright/test';
import { adminuser, apiUrl } from './defaults';
export const createApi = () =>
request.newContext({
baseURL: apiUrl,
extraHTTPHeaders: {
Authorization: `Basic ${btoa(`${adminuser.username}:${adminuser.password}`)}`
}
});

View File

@@ -1,3 +1,6 @@
import { expect } from '@playwright/test';
import { createApi } from './api';
/**
* Open the filter drawer for the currently visible table
* @param page - The page object
@@ -130,3 +133,20 @@ export const globalSearch = async (page, query) => {
await page.getByPlaceholder('Enter search text').fill(query);
await page.waitForTimeout(300);
};
export const deletePart = async (name: string) => {
const api = await createApi();
const parts = await api
.get('part/', {
params: { search: name }
})
.then((res) => res.json());
const existingPart = parts.find((p: any) => p.name === name);
if (existingPart) {
await api.patch(`part/${existingPart.pk}/`, {
data: { active: false }
});
const res = await api.delete(`part/${existingPart.pk}/`);
expect(res.status()).toBe(204);
}
};

View File

@@ -39,10 +39,9 @@ test('Dashboard - Basic', async ({ browser }) => {
await page.getByLabel('dashboard-accept-layout').click();
});
test('Dashboard - Plugins', async ({ browser, request }) => {
test('Dashboard - Plugins', async ({ browser }) => {
// Ensure that the "SampleUI" plugin is enabled
await setPluginState({
request,
plugin: 'sampleui',
state: true
});

View File

@@ -2,12 +2,14 @@ import { test } from '../baseFixtures';
import {
clearTableFilters,
clickOnRowMenu,
deletePart,
getRowFromCell,
loadTab,
navigate,
setTableChoiceFilter
} from '../helpers';
import { doCachedLogin } from '../login';
import { setPluginState, setSettingState } from '../settings';
/**
* CHeck each panel tab for the "Parts" page
@@ -645,3 +647,62 @@ test('Parts - Duplicate', async ({ browser }) => {
await page.getByText('Copy Parameters', { exact: true }).waitFor();
await page.getByText('Copy Tests', { exact: true }).waitFor();
});
test('Parts - Import supplier part', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'part/category/1/parts'
});
// Ensure that the sample supplier plugin is enabled
await setPluginState({
plugin: 'samplesupplier',
state: true
});
await setSettingState({
setting: 'SUPPLIER',
value: 3,
type: 'plugin',
plugin: 'samplesupplier'
});
// cleanup old imported part if it exists
await deletePart('BOLT-Steel-M5-5');
await deletePart('BOLT-M5-5');
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.getByRole('button', { name: 'action-button-import-part' }).click();
await page
.getByRole('textbox', { name: 'textbox-search-for-part' })
.fill('M5');
await page.waitForTimeout(250);
await page
.getByRole('textbox', { name: 'textbox-search-for-part' })
.press('Enter');
await page.getByText('Bolt M5x5mm Steel').waitFor();
await page
.getByRole('button', { name: 'action-button-import-part-BOLT-Steel-M5-5' })
.click();
await page.waitForTimeout(250);
await page
.getByRole('button', { name: 'action-button-import-part-now' })
.click();
await page
.getByRole('button', { name: 'action-button-import-create-parameters' })
.dispatchEvent('click');
await page
.getByRole('button', { name: 'action-button-import-stock-next' })
.dispatchEvent('click');
await page
.getByRole('button', { name: 'action-button-import-close' })
.dispatchEvent('click');
// cleanup imported part if it exists
await deletePart('BOLT-Steel-M5-5');
await deletePart('BOLT-M5-5');
});

View File

@@ -18,7 +18,7 @@ test('Machines - Admin Panel', async ({ browser }) => {
await page.getByText('There are no machine registry errors').waitFor();
});
test('Machines - Activation', async ({ browser, request }) => {
test('Machines - Activation', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree',
@@ -27,7 +27,6 @@ test('Machines - Activation', async ({ browser, request }) => {
// Ensure that the sample machine plugin is enabled
await setPluginState({
request,
plugin: 'sample-printer-machine-plugin',
state: true
});

View File

@@ -10,7 +10,7 @@ import { doCachedLogin } from './login';
* Test the "admin" account
* - This is a superuser account, so should have *all* permissions available
*/
test('Permissions - Admin', async ({ browser, request }) => {
test('Permissions - Admin', async ({ browser }) => {
// Login, and start on the "admin" page
const page = await doCachedLogin(browser, {
username: 'admin',
@@ -57,7 +57,7 @@ test('Permissions - Admin', async ({ browser, request }) => {
* Test the "reader" account
* - This account is read-only, but should be able to access *most* pages
*/
test('Permissions - Reader', async ({ browser, request }) => {
test('Permissions - Reader', async ({ browser }) => {
// Login, and start on the "admin" page
const page = await doCachedLogin(browser, {
username: 'reader',

View File

@@ -11,7 +11,7 @@ import { doCachedLogin } from './login.js';
import { setPluginState, setSettingState } from './settings.js';
// Unit test for plugin settings
test('Plugins - Settings', async ({ browser, request }) => {
test('Plugins - Settings', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
@@ -19,7 +19,6 @@ test('Plugins - Settings', async ({ browser, request }) => {
// Ensure that the SampleIntegration plugin is enabled
await setPluginState({
request,
plugin: 'sample',
state: true
});
@@ -63,12 +62,11 @@ test('Plugins - Settings', async ({ browser, request }) => {
await page.getByText('Mouser Electronics').click();
});
test('Plugins - User Settings', async ({ browser, request }) => {
test('Plugins - User Settings', async ({ browser }) => {
const page = await doCachedLogin(browser);
// Ensure that the SampleIntegration plugin is enabled
await setPluginState({
request,
plugin: 'sample',
state: true
});
@@ -149,7 +147,7 @@ test('Plugins - Functionality', async ({ browser }) => {
.waitFor();
});
test('Plugins - Panels', async ({ browser, request }) => {
test('Plugins - Panels', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
@@ -157,14 +155,12 @@ test('Plugins - Panels', async ({ browser, request }) => {
// Ensure that UI plugins are enabled
await setSettingState({
request,
setting: 'ENABLE_PLUGINS_INTERFACE',
value: true
});
// Ensure that the SampleUI plugin is enabled
await setPluginState({
request,
plugin: 'sampleui',
state: true
});
@@ -192,7 +188,6 @@ test('Plugins - Panels', async ({ browser, request }) => {
// Disable the plugin, and ensure it is no longer visible
await setPluginState({
request,
plugin: 'sampleui',
state: false
});
@@ -201,7 +196,7 @@ test('Plugins - Panels', async ({ browser, request }) => {
/**
* Unit test for custom admin integration for plugins
*/
test('Plugins - Custom Admin', async ({ browser, request }) => {
test('Plugins - Custom Admin', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
@@ -209,7 +204,6 @@ test('Plugins - Custom Admin', async ({ browser, request }) => {
// Ensure that the SampleUI plugin is enabled
await setPluginState({
request,
plugin: 'sampleui',
state: true
});
@@ -235,7 +229,7 @@ test('Plugins - Custom Admin', async ({ browser, request }) => {
await page.getByText('hello: world').waitFor();
});
test('Plugins - Locate Item', async ({ browser, request }) => {
test('Plugins - Locate Item', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
@@ -243,7 +237,6 @@ test('Plugins - Locate Item', async ({ browser, request }) => {
// Ensure that the sample location plugin is enabled
await setPluginState({
request,
plugin: 'samplelocate',
state: true
});

View File

@@ -77,7 +77,7 @@ test('Printing - Report Printing', async ({ browser }) => {
await page.context().close();
});
test('Printing - Report Editing', async ({ browser, request }) => {
test('Printing - Report Editing', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
@@ -85,7 +85,6 @@ test('Printing - Report Editing', async ({ browser, request }) => {
// activate the sample plugin for this test
await setPluginState({
request,
plugin: 'sampleui',
state: true
});
@@ -140,7 +139,6 @@ test('Printing - Report Editing', async ({ browser, request }) => {
// deactivate the sample plugin again after the test
await setPluginState({
request,
plugin: 'sampleui',
state: false
});

View File

@@ -1,5 +1,5 @@
import { createApi } from './api.js';
import { expect, test } from './baseFixtures.js';
import { apiUrl } from './defaults.js';
import { getRowFromCell, loadTab, navigate } from './helpers.js';
import { doCachedLogin } from './login.js';
import { setPluginState, setSettingState } from './settings.js';
@@ -134,7 +134,7 @@ test('Settings - User', async ({ browser }) => {
.waitFor();
});
test('Settings - Global', async ({ browser, request }) => {
test('Settings - Global', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
@@ -144,7 +144,6 @@ test('Settings - Global', async ({ browser, request }) => {
// Ensure the "slack" notification plugin is enabled
// This is to ensure it is visible in the "notification" settings tab
await setPluginState({
request,
plugin: 'inventree-slack-notification',
state: true
});
@@ -312,7 +311,7 @@ test('Settings - Admin', async ({ browser }) => {
await page.getByRole('button', { name: 'Submit' }).click();
});
test('Settings - Admin - Barcode History', async ({ browser, request }) => {
test('Settings - Admin - Barcode History', async ({ browser }) => {
// Login with admin credentials
const page = await doCachedLogin(browser, {
username: 'admin',
@@ -321,25 +320,21 @@ test('Settings - Admin - Barcode History', async ({ browser, request }) => {
// Ensure that the "save scans" setting is enabled
await setSettingState({
request: request,
setting: 'BARCODE_STORE_RESULTS',
value: true
});
// Scan some barcodes (via API calls)
const barcodes = ['ABC1234', 'XYZ5678', 'QRS9012'];
const api = await createApi();
for (let i = 0; i < barcodes.length; i++) {
const barcode = barcodes[i];
const url = new URL('barcode/', apiUrl).toString();
await request.post(url, {
await api.post('barcode/', {
data: {
barcode: barcode
},
timeout: 5000,
headers: {
Authorization: `Basic ${btoa('admin:inventree')}`
}
timeout: 5000
});
}

View File

@@ -1,54 +1,48 @@
import { expect } from 'playwright/test';
import { apiUrl } from './defaults';
import { createApi } from './api';
/*
* Set the value of a global setting in the database
*/
export const setSettingState = async ({
request,
setting,
value
value,
type = 'global',
plugin
}: {
request: any;
setting: string;
value: any;
type?: 'global' | 'plugin';
plugin?: string;
}) => {
const url = new URL(`settings/global/${setting}/`, apiUrl).toString();
const response = await request.patch(url, {
const api = await createApi();
const url =
type === 'global'
? `settings/global/${setting}/`
: `plugins/${plugin}/settings/${setting}/`;
const response = await api.patch(url, {
data: {
value: value
},
headers: {
// Basic username: password authorization
Authorization: `Basic ${btoa('admin:inventree')}`
}
});
expect(await response.status()).toBe(200);
expect(response.status()).toBe(200);
};
export const setPluginState = async ({
request,
plugin,
state
}: {
request: any;
plugin: string;
state: boolean;
}) => {
const url = new URL(`plugins/${plugin}/activate/`, apiUrl).toString();
const response = await request.patch(url, {
const api = await createApi();
const response = await api.patch(`plugins/${plugin}/activate/`, {
data: {
active: state
},
headers: {
// Basic username: password authorization
Authorization: `Basic ${btoa('admin:inventree')}`
}
});
expect(await response.status()).toBe(200);
expect(response.status()).toBe(200);
};