2
0
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 commit d555658db2.

* 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 commit 477c692076.

* 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:
Oliver
2025-12-04 20:41:36 +11:00
committed by GitHub
parent c443b4e9b8
commit fa0d892a62
135 changed files with 5873 additions and 3307 deletions

View File

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

View File

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

View File

@@ -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

View File

@@ -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',

View File

@@ -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');