mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-17 09:48:30 +00:00
[WIP] Generic parameters (#10699)
* Add ParameterTemplate model - Data structure duplicated from PartParameterTemplate * Apply data migration for templates * Admin integration * API endpoints for ParameterTemplate * Scaffolding * Add validator for ParameterTemplate model type - Update migrations - Make Parameter class abstract (for now) - Validators * API updates - Fix options for model_type - Add API filters * Add definition for Parameter model * Add django admin site integration * Update InvenTreeParameterMixin class - Fetch queryset of all linked Parameter instances - Ensure deletion of linked instances * API endpoints for Parameter instances * Refactor UI table for parameter templates * Add comment for later * Add "enabled" field to ParameterTemplate model * Add new field to serializer * Rough-in new table * Implement generic "parameter" table * Enable parameters for Company model * Change migration for part parameter - Make it "universal" * Remove code for ManufacturerPartParameter * Fix for filters * Add data import for parameter table * Add verbose name to ParameterTemplate model * Removed dead API code * Update global setting * Fix typos * Check global setting for unit validation * Use GenericForeignKey * Add generic relationship to allow reverse lookups * Fixes for table structure * Add custom serializer field for ContentType with choices * Adds ContentTypeField - Handles representation of content type - Provides human-readable options * Refactor API filtering for endpoints - Specify ContentType by ID, model or app label * Revert change to parameters property * Define GenericRelationship for linking model * Refactoring some code * Add a generic way to back-annotate and prefetch parameters for any model type * Change panel position * Directly annotate parameters against different model serializers * remove defunct admin classes * Run plugin validation against parameter * Fix prefetching for PartSerializer * Implement generic "filtering" against queryset * Implement generic "ordering" by parameter * Make parametric table generic * Refactor segmented panels * Consolidate part table views * Fix for parametric part table - Only display parameters for which we know there is a value * Add parametric tables for company views * Fix typo in file name * Prefetch to reduce hits * Add generic API mixin for filtering and ordering by parameter * Fix hook for rebuilding template parameters * Remove serializer * Remove old models * Fix code for copying parameters from category * Implement more parametric tables: - ManufacturerPart - SupplierPart - Fixes and enhancements * Add parameter support for orders * Add UI support for parameters against orders * Update API version * Update CHANGELOG.md * Add parameter support for build orders * Tweak frontend * Add renderer * Remove defunct endpoints * Add migration requirement * Require contenttypes to be updated * Update migration * Try using ID val * Adjust migration dependencies * fix params fixture * fix schema export * fix modelset * Fixes for data migration * tweak table * Fix for Category Parameters * Use branch of demo dataset for testing * Add parameteric build order table * disable broken imports * remove old model from ruleset * correct test * Table tweaks * fix test * Remove old model type * fix test * fix test * Refactor mixin to avoid specifying model type manually * fix test * fix resolve name * remove unneeded import * Tweak unit testing * Fix unit test * Enable bulk-create * More fixes * More unit test tweaks * Enhancements * Unit test fixes * Add some migration tests * Fix admin tests * Fix part tests * adapt expectation * fix remaining typecheck * Docs updates * Rearrange models * fix paramater caching * fix doc links * adjust assumption * Adjust data migration unit tests * docs fixes * Fix docs link * Fixes * Tweak formatting * Add doc for setting * Add metadata view for parameters * Add metadata view for ParamterTemplate * Update CHANGELOG file * Deconflict model_type fields * Invert key:value * Revert "Invert key:value" This reverts commitd555658db2. * fix assert * Update API rev notes * Initial unit tests for API * Test parameter create / edit / delete via the API * Add some more unit tests for the API * Validate queryset annotation - Add unit test with large dataset - Ensure number of queries is fixed - Fix for prefetching check * Add breaking change info to CHANGELOG.md * Ensure that parameters are removed when deleting the linked object * Enhance type hinting * Refactor part parameter exporter plugin - Any model which supports parameters can use this now - Update documentation * Improve serializer field * Adjust unit test * Reimplement checks for locked parts * Fix unit test for data migration * Fix for unit test * Allow disable edit for ParameterTable * Fix supplier part import wizard * Add unit tests for template API filtering * Add playwright tests for purchasing index * Add tests for manufacturing index page * ui tests for sales index * Add data migration tests for ManufacturerPartParameter * Pull specific branch for python binding tests * Specify target migration * Remove debug statement * Tweak migration unit tests * Add options for spectacular * Add explicit choice options * Ensure empty string values are converted to None * Don't use custom branch for python checks * Fix for migration test * Fix migration test * Fix reference target * Remove duplicate enum in spectactular.py * Add null choice to custom serializer class * [UI] Edit shipment details - Pass "pending" status through to the form * New migration strategy: part.0144: - Add new "enabled" field to PartParameterTemplate model - Add new ContentType fields to the "PartParameterTemplate" and "PartParameter" models - Data migration for existing "PartParameter" records part.0145: - Set NOT NULL constraints on new fields - Remove the obsolete "part" field from the "PartParameter" model * More migration updates: - Create new "models" (without moving the existing tables) - Data migration for PartCataegoryParameterTemplate model - Remove PartParameterTemplate and PartParameter models * Overhaul of migration strategy - New models simply point to the old database tables - Perform schema and data migrations on the old models first (in the part app) - Swap model references in correct order * Improve checks for data migrations * Bug fix for data migration * Add migration unit test to ensure that primary keys are maintained * Add playwright test for company parameters * Rename underlying database tables * Fixes for migration unit tests * Revert "Rename underlying database tables" This reverts commit477c692076. * Fix for migration sequencing * Simplify new playwright test * Remove spectacular collision * Monkey patch the drf-spectacular warn function * Do not use custom branch for playwright testing --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -11,6 +11,26 @@ import {
|
||||
} from '../helpers.ts';
|
||||
import { doCachedLogin } from '../login.ts';
|
||||
|
||||
test('Build - Index', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'manufacturing/index/' });
|
||||
|
||||
await loadTab(page, 'Build Orders');
|
||||
|
||||
// Ensure all data views are available
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'action-button-next-month' }).click();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
});
|
||||
|
||||
test('Build Order - Basic Tests', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test } from '../baseFixtures.js';
|
||||
import { loadTab, navigate } from '../helpers.js';
|
||||
import { clickOnParamFilter, loadTab, navigate } from '../helpers.js';
|
||||
import { doCachedLogin } from '../login.js';
|
||||
|
||||
test('Company', async ({ browser }) => {
|
||||
@@ -40,3 +40,23 @@ test('Company', async ({ browser }) => {
|
||||
await page.getByText('Enter a valid URL.').waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
|
||||
test('Company - Parameters', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
username: 'steven',
|
||||
password: 'wizardstaff',
|
||||
url: 'purchasing/index/suppliers'
|
||||
});
|
||||
|
||||
// Show parametric view
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
// Filter by "payment terms" parameter value
|
||||
await clickOnParamFilter(page, 'Payment Terms');
|
||||
await page.getByRole('option', { name: 'NET-30' }).click();
|
||||
|
||||
await page.getByRole('cell', { name: 'Arrow Electronics' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'PCB assembly house' }).waitFor();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test } from '../baseFixtures';
|
||||
import {
|
||||
clearTableFilters,
|
||||
clickOnParamFilter,
|
||||
clickOnRowMenu,
|
||||
deletePart,
|
||||
getRowFromCell,
|
||||
@@ -182,7 +183,14 @@ test('Parts - Locking', async ({ browser }) => {
|
||||
.waitFor();
|
||||
|
||||
await loadTab(page, 'Parameters');
|
||||
await page.getByLabel('action-button-add-parameter').waitFor();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-menu-add-parameters' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-parameters-create-parameter'
|
||||
})
|
||||
.click();
|
||||
|
||||
// Navigate to a known assembly which *is* locked
|
||||
await navigate(page, 'part/100/bom');
|
||||
@@ -495,7 +503,14 @@ test('Parts - Parameters', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'part/69/parameters' });
|
||||
|
||||
// Create a new template
|
||||
await page.getByLabel('action-button-add-parameter').click();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-menu-add-parameters' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-parameters-create-parameter'
|
||||
})
|
||||
.click();
|
||||
|
||||
// Select the "Color" parameter template (should create a "choice" field)
|
||||
await page.getByLabel('related-field-template').fill('Color');
|
||||
@@ -509,7 +524,7 @@ test('Parts - Parameters', async ({ browser }) => {
|
||||
|
||||
// Select the "polarized" parameter template (should create a "checkbox" field)
|
||||
await page.getByLabel('related-field-template').fill('Polarized');
|
||||
await page.getByText('Is this part polarized?').click();
|
||||
await page.getByRole('option', { name: 'Polarized Is this part' }).click();
|
||||
|
||||
// Submit with "false" value
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
@@ -538,38 +553,33 @@ test('Parts - Parameters', async ({ browser }) => {
|
||||
// Finally, delete the parameter
|
||||
await row.getByLabel(/row-action-menu-/i).click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click();
|
||||
await page.getByText('No records found').first().waitFor();
|
||||
});
|
||||
|
||||
test('Parts - Parameter Filtering', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'part/' });
|
||||
|
||||
await loadTab(page, 'Part Parameters');
|
||||
await loadTab(page, 'Parts', true);
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
await clearTableFilters(page);
|
||||
|
||||
// All parts should be available (no filters applied)
|
||||
await page.getByText(/\/ 42\d/).waitFor();
|
||||
|
||||
const clickOnParamFilter = async (name: string) => {
|
||||
const button = await page
|
||||
.getByRole('button', { name: `${name} Not sorted` })
|
||||
.getByRole('button')
|
||||
.first();
|
||||
await button.scrollIntoViewIfNeeded();
|
||||
await button.click();
|
||||
};
|
||||
|
||||
const clearParamFilter = async (name: string) => {
|
||||
await clickOnParamFilter(name);
|
||||
await clickOnParamFilter(page, name);
|
||||
await page.getByLabel(`clear-filter-${name}`).waitFor();
|
||||
await page.getByLabel(`clear-filter-${name}`).click();
|
||||
// await page.getByLabel(`clear-filter-${name}`).click();
|
||||
};
|
||||
|
||||
// Let's filter by color
|
||||
await clickOnParamFilter('Color');
|
||||
await clickOnParamFilter(page, 'Color');
|
||||
await page.getByRole('option', { name: 'Red' }).click();
|
||||
|
||||
// Only 10 parts available
|
||||
|
||||
@@ -13,6 +13,117 @@ import {
|
||||
} from '../helpers.ts';
|
||||
import { doCachedLogin } from '../login.ts';
|
||||
|
||||
test('Purchasing - Index', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'purchasing/index/' });
|
||||
|
||||
// Purchase Orders tab
|
||||
await loadTab(page, 'Purchase Orders');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'calendar-select-month' }).waitFor();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Suppliers tab
|
||||
await loadTab(page, 'Suppliers');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Supplier parts tab
|
||||
await loadTab(page, 'Supplier Parts');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Manufacturers tab
|
||||
await loadTab(page, 'Manufacturers');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Manufacturer parts tab
|
||||
await loadTab(page, 'Manufacturer Parts');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
});
|
||||
|
||||
test('Purchase Orders - General', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.waitForURL('**/purchasing/index/**');
|
||||
|
||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Line Items');
|
||||
await loadTab(page, 'Received Stock');
|
||||
await loadTab(page, 'Parameters');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Suppliers');
|
||||
await page.getByText('Arrow', { exact: true }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Supplied Parts');
|
||||
await loadTab(page, 'Purchase Orders');
|
||||
await loadTab(page, 'Stock Items');
|
||||
await loadTab(page, 'Contacts');
|
||||
await loadTab(page, 'Addresses');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Manufacturers');
|
||||
await page.getByText('AVX Corporation').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Addresses');
|
||||
await page.getByRole('cell', { name: 'West Branch' }).click();
|
||||
await page.locator('.mantine-ScrollArea-root').click();
|
||||
await page
|
||||
.getByRole('row', { name: 'West Branch Yes Surf Avenue 9' })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await page.getByLabel('text-field-title', { exact: true }).waitFor();
|
||||
await page.getByLabel('text-field-line2', { exact: true }).waitFor();
|
||||
|
||||
// Read the current value of the cell, to ensure we always *change* it!
|
||||
const value = await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.inputValue();
|
||||
await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.fill(value == 'old' ? 'new' : 'old');
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
||||
});
|
||||
|
||||
test('Purchase Orders - Table', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
@@ -130,62 +241,6 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
|
||||
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
||||
});
|
||||
|
||||
test('Purchase Orders - General', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.waitForURL('**/purchasing/index/**');
|
||||
|
||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Line Items');
|
||||
await loadTab(page, 'Received Stock');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Suppliers');
|
||||
await page.getByText('Arrow', { exact: true }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Supplied Parts');
|
||||
await loadTab(page, 'Purchase Orders');
|
||||
await loadTab(page, 'Stock Items');
|
||||
await loadTab(page, 'Contacts');
|
||||
await loadTab(page, 'Addresses');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Manufacturers');
|
||||
await page.getByText('AVX Corporation').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Addresses');
|
||||
await page.getByRole('cell', { name: 'West Branch' }).click();
|
||||
await page.locator('.mantine-ScrollArea-root').click();
|
||||
await page
|
||||
.getByRole('row', { name: 'West Branch Yes Surf Avenue 9' })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await page.getByLabel('text-field-title', { exact: true }).waitFor();
|
||||
await page.getByLabel('text-field-line2', { exact: true }).waitFor();
|
||||
|
||||
// Read the current value of the cell, to ensure we always *change* it!
|
||||
const value = await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.inputValue();
|
||||
await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.fill(value == 'old' ? 'new' : 'old');
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
||||
});
|
||||
|
||||
test('Purchase Orders - Filters', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
username: 'reader',
|
||||
|
||||
@@ -18,6 +18,16 @@ test('Sales Orders - Tabs', async ({ browser }) => {
|
||||
await loadTab(page, 'Sales Orders');
|
||||
await page.waitForURL('**/web/sales/index/salesorders');
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Pending Shipments panel
|
||||
await loadTab(page, 'Pending Shipments');
|
||||
await page.getByRole('cell', { name: 'SO0007' }).waitFor();
|
||||
@@ -27,8 +37,26 @@ test('Sales Orders - Tabs', async ({ browser }) => {
|
||||
await loadTab(page, 'Return Orders');
|
||||
await page.getByRole('cell', { name: 'NOISE-COMPLAINT' }).waitFor();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Customers
|
||||
await loadTab(page, 'Customers');
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
await page.getByText('Customer A').click();
|
||||
await loadTab(page, 'Notes');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
Reference in New Issue
Block a user