mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-12 03:28:37 +00:00
Support physical units for BOM lines (#11631)
* Add new "raw_amount" field to BomItem model * Batch process data migration * Update migration * Calculate 'quantity' from 'raw_amount' field * Improve decimal formatting in migration * Allow raw_amount in serializer * Adjust frontend form * API validation and unit tests * Additional playwright tests * Update API version and CHANGELOG * Updated docs * Fix docstring * Better handling of empty values * Tweak unit tests * Tweak unit test * Fix unit test * Adjust form field text * Adjust migration file * Tweak playwright tests * Fix unit test * Adjust serializers / import-export / playwright * Fix migration * Fix validation * Loosen comparison --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -38,7 +38,10 @@ export function bomItemFields({
|
||||
},
|
||||
addCreateFields: newPartFields
|
||||
},
|
||||
quantity: {},
|
||||
raw_amount: {
|
||||
label: t`Quantity`,
|
||||
description: t`Required component quantity`
|
||||
},
|
||||
reference: {},
|
||||
setup_quantity: {},
|
||||
attrition: {},
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
Assembly,Component,Reference,Quantity,Allow Variants,Gets inherited,Optional,Consumable,Setup quantity,Attrition,Rounding multiple,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
|
||||
Assembly,Component,Reference,Amount,Allow Variants,Gets inherited,Optional,Consumable,Setup quantity,Attrition,Rounding multiple,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
|
||||
106,98,screws,5,FALSE,TRUE,FALSE,TRUE,0,0,0,,39,0.075,0.1,0.375,0.5,23/07/2025 9:12,,Wood Screw,Screw for fixing wood to other wood,TRUE,1604,0,0,0,0,0,320.8
|
||||
106,95,legs,4,FALSE,TRUE,FALSE,FALSE,0,0,0,,40,10.6,12.75,42.4,51,23/07/2025 9:12,,Leg,Leg for a chair or a table,TRUE,317,0,0,0,0,0,79.25
|
||||
109,92,paint,0.125,FALSE,FALSE,FALSE,FALSE,0,0,0,,43,1.403886,14.389836,0.175486,1.79873,23/07/2025 9:12,,Green Paint,Green Paint,TRUE,110.125,0,0,0,0,0,881
|
||||
109,92,paint,quart,FALSE,FALSE,FALSE,FALSE,0,0,0,,43,1.403886,14.389836,0.175486,1.79873,23/07/2025 9:12,,Green Paint,Green Paint,TRUE,110.125,0,0,0,0,0,881
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '../baseFixtures';
|
||||
import {
|
||||
clearTableFilters,
|
||||
@@ -219,6 +220,53 @@ test('Parts - BOM', async ({ browser }) => {
|
||||
await page.getByRole('button', { name: 'Add Substitute' }).waitFor();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
|
||||
// Let's try a BOM which has a "raw amount" which considers the units of the underlying part
|
||||
await navigate(page, 'part/109/bom');
|
||||
|
||||
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
|
||||
|
||||
const paintCell = await page.getByRole('cell', {
|
||||
name: 'Thumbnail Green Paint'
|
||||
});
|
||||
await clickOnRowMenu(paintCell);
|
||||
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'text-field-raw_amount' })
|
||||
).toHaveValue(/quart/);
|
||||
|
||||
// Try to assign invalid units to this item, which should be rejected by validation
|
||||
await page
|
||||
.getByRole('textbox', { name: 'text-field-raw_amount' })
|
||||
.fill('2 cm');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.getByText('Errors exist for one or more').waitFor();
|
||||
await page.getByText('Could not convert 2 cm to litres').waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Create a new BOM item with valid units
|
||||
await page.getByRole('button', { name: 'action-menu-add-bom-items' }).click();
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'action-menu-add-bom-items-add' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('combobox', { name: 'related-field-sub_part' })
|
||||
.fill('red wire');
|
||||
await page
|
||||
.getByRole('option', { name: 'Thumbnail Silicon Wire 12AWG' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'text-field-reference' })
|
||||
.fill('my-ref');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'text-field-raw_amount' })
|
||||
.fill('3/4 inches');
|
||||
await page.getByRole('switch', { name: 'boolean-field-optional' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Check for the value converted back to [m]
|
||||
await page.getByRole('cell', { name: '0.01905' }).first().waitFor();
|
||||
await page.getByRole('cell', { name: 'my-ref' }).first().waitFor();
|
||||
// Finish editing the BOM
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-finish-editing-' })
|
||||
@@ -240,13 +288,15 @@ test('Parts - BOM Validation', async ({ browser }) => {
|
||||
.waitFor();
|
||||
|
||||
// Edit line item, to ensure BOM is not valid
|
||||
const cell = await page.getByRole('cell', { name: 'Thumbnail Red Paint' });
|
||||
const cell = await page.getByRole('cell', { name: 'paint', exact: true });
|
||||
|
||||
// await cell.click({ button: 'right' });
|
||||
// await page.getByRole('button', { name: 'Edit', exact: true }).click();
|
||||
await clickOnRowMenu(cell);
|
||||
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
||||
|
||||
const input = await page.getByRole('textbox', {
|
||||
name: 'number-field-quantity'
|
||||
name: 'text-field-raw_amount'
|
||||
});
|
||||
|
||||
const value = await input.inputValue();
|
||||
@@ -901,7 +951,7 @@ test('Parts - Parameter Filtering', async ({ browser }) => {
|
||||
test('Parts - Test Results', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: '/part/74/test_results' });
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await page.getByText(/1 - \d+ \/ 1\d\d/).waitFor();
|
||||
await page.getByText('Blue Paint Applied').waitFor();
|
||||
|
||||
@@ -138,7 +138,7 @@ test('Importing - BOM', async ({ browser }) => {
|
||||
.getByLabel('row-action-menu-')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
await page.getByRole('textbox', { name: 'number-field-quantity' }).fill('12');
|
||||
await page.getByRole('textbox', { name: 'text-field-raw_amount' }).fill('12');
|
||||
|
||||
await page.waitForTimeout(250);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
@@ -401,7 +401,7 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
|
||||
await loadTab(page, 'Parameters', true);
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Clean old template data if exists
|
||||
await page
|
||||
|
||||
Reference in New Issue
Block a user