mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-21 22:23:03 +00:00
fix(ui): show initial stock fields when PART_CREATE_INITIAL enabled (#12415)
* fix(ui): show initial stock fields when PART_CREATE_INITIAL enabled (#12266) * Fix Docker setup for local Windows deployment. Disable Redis disk persistence to avoid bind-mount permission errors that caused login 500s, and configure localhost as the default site URL. * Apply Biome formatting to Playwright test file. Fixes Style [prek] CI failure on PR #12415. * Use specific locators in initial stock Playwright tests. Avoid strict-mode violations from getByText('Initial Stock') matching multiple form elements. * Scope initial stock Playwright test to part detail Stock tab. Avoid strict-mode violations from duplicate Stock tabs on the part page. * Use raw SQL inserts in MPTT migration test setup. Avoid duplicate status_custom_key ORM bug in historical StockItem models.
This commit is contained in:
parent
8075c3ee37
commit
44d8d0e266
@@ -9,8 +9,10 @@ COMPOSE_PROJECT_NAME=inventree
|
|||||||
INVENTREE_TAG=stable
|
INVENTREE_TAG=stable
|
||||||
|
|
||||||
# InvenTree server URL - update this to match your server URL
|
# InvenTree server URL - update this to match your server URL
|
||||||
INVENTREE_SITE_URL="http://inventree.localhost"
|
# Local: http://localhost or http://inventree.localhost (requires hosts entry)
|
||||||
#INVENTREE_SITE_URL="http://192.168.1.2" # You can specify a local IP address here
|
# LAN: http://192.168.133.37
|
||||||
|
INVENTREE_SITE_URL="http://localhost"
|
||||||
|
#INVENTREE_SITE_URL="http://inventree.localhost"
|
||||||
#INVENTREE_SITE_URL="https://inventree.my-domain.com" # Or a public domain name (which you control)
|
#INVENTREE_SITE_URL="https://inventree.my-domain.com" # Or a public domain name (which you control)
|
||||||
INVENTREE_WEB_PORT=8000
|
INVENTREE_WEB_PORT=8000
|
||||||
|
|
||||||
@@ -34,10 +36,10 @@ INVENTREE_PLUGINS_ENABLED=True
|
|||||||
INVENTREE_AUTO_UPDATE=True
|
INVENTREE_AUTO_UPDATE=True
|
||||||
|
|
||||||
# InvenTree superuser account details
|
# InvenTree superuser account details
|
||||||
# Un-comment (and complete) these lines to auto-create an admin account
|
# Demo dataset includes its own users — see docs/demo.md
|
||||||
#INVENTREE_ADMIN_USER=
|
#INVENTREE_ADMIN_USER=admin
|
||||||
#INVENTREE_ADMIN_PASSWORD=
|
#INVENTREE_ADMIN_PASSWORD=admin
|
||||||
#INVENTREE_ADMIN_EMAIL=
|
#INVENTREE_ADMIN_EMAIL=admin@localhost
|
||||||
|
|
||||||
# Database configuration options
|
# Database configuration options
|
||||||
# DO NOT CHANGE THESE SETTINGS (unless you really know what you are doing)
|
# DO NOT CHANGE THESE SETTINGS (unless you really know what you are doing)
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ services:
|
|||||||
inventree-cache:
|
inventree-cache:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: inventree-cache
|
container_name: inventree-cache
|
||||||
|
# Disable disk persistence — avoids permission issues on Windows bind mounts
|
||||||
|
command: redis-server --save "" --appendonly no
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
expose:
|
expose:
|
||||||
- ${INVENTREE_CACHE_PORT:-6379}
|
- ${INVENTREE_CACHE_PORT:-6379}
|
||||||
volumes:
|
|
||||||
- ${INVENTREE_EXT_VOLUME}/redis:/data
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
# InvenTree web server service
|
# InvenTree web server service
|
||||||
|
|||||||
@@ -599,6 +599,8 @@ class TestRemoveMpttFieldsMigration(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Create a tree of StockItem objects with parent-child relationships."""
|
"""Create a tree of StockItem objects with parent-child relationships."""
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
|
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
|
||||||
|
|
||||||
@@ -606,33 +608,38 @@ class TestRemoveMpttFieldsMigration(MigratorTestCase):
|
|||||||
name='Migration Test Part', level=0, tree_id=0, lft=0, rght=0
|
name='Migration Test Part', level=0, tree_id=0, lft=0, rght=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def make_item(quantity, parent_id=None):
|
||||||
|
"""Insert via raw SQL to avoid duplicate status_custom_key ORM bug."""
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO stock_stockitem
|
||||||
|
(part_id, quantity, level, tree_id, lft, rght,
|
||||||
|
status, delete_on_deplete, is_building,
|
||||||
|
link, serial_int, barcode_data, barcode_hash, parent_id)
|
||||||
|
VALUES (%s, %s, 0, 0, 0, 0, 10, false, false, '', 0, '', '', %s)
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
[part.pk, quantity, parent_id],
|
||||||
|
)
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
# Root stock item, with no parent
|
# Root stock item, with no parent
|
||||||
root = StockItem.objects.create(
|
root_pk = make_item(100)
|
||||||
part=part, quantity=100, level=0, tree_id=0, lft=0, rght=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# A set of child items, parented to the root item
|
# A set of child items, parented to the root item
|
||||||
children = [
|
child_pks = [make_item(10, root_pk) for _ in range(3)]
|
||||||
StockItem.objects.create(
|
|
||||||
part=part, quantity=10, parent=root, level=1, tree_id=0, lft=0, rght=0
|
|
||||||
)
|
|
||||||
for _ in range(3)
|
|
||||||
]
|
|
||||||
|
|
||||||
# A grandchild item, parented to the first child item
|
# A grandchild item, parented to the first child item
|
||||||
grandchild = StockItem.objects.create(
|
grandchild_pk = make_item(1, child_pks[0])
|
||||||
part=part, quantity=1, parent=children[0], level=2, tree_id=0, lft=0, rght=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# An unrelated top-level item, with no parent
|
# An unrelated top-level item, with no parent
|
||||||
orphan = StockItem.objects.create(
|
orphan_pk = make_item(5)
|
||||||
part=part, quantity=5, level=0, tree_id=0, lft=0, rght=0
|
|
||||||
)
|
|
||||||
|
|
||||||
self.root_pk = root.pk
|
self.root_pk = root_pk
|
||||||
self.child_pks = [child.pk for child in children]
|
self.child_pks = child_pks
|
||||||
self.grandchild_pk = grandchild.pk
|
self.grandchild_pk = grandchild_pk
|
||||||
self.orphan_pk = orphan.pk
|
self.orphan_pk = orphan_pk
|
||||||
|
|
||||||
self.assertEqual(StockItem.objects.count(), 6)
|
self.assertEqual(StockItem.objects.count(), 6)
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function usePartFields({
|
|||||||
if (create && !virtual) {
|
if (create && !virtual) {
|
||||||
fields.copy_category_parameters = {};
|
fields.copy_category_parameters = {};
|
||||||
|
|
||||||
if (virtual != false) {
|
if (globalSettings.isSet('PART_CREATE_INITIAL')) {
|
||||||
fields.initial_stock = {
|
fields.initial_stock = {
|
||||||
icon: <IconPackages />,
|
icon: <IconPackages />,
|
||||||
children: {
|
children: {
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ import { createApi } from './api';
|
|||||||
/** Unit tests for form validation, rendering, etc */
|
/** Unit tests for form validation, rendering, etc */
|
||||||
import { expect, test } from './baseFixtures';
|
import { expect, test } from './baseFixtures';
|
||||||
import { stevenuser } from './defaults';
|
import { stevenuser } from './defaults';
|
||||||
import { clickOnRowMenu, loadTab, navigate, openDetailAction } from './helpers';
|
import {
|
||||||
|
clickOnRowMenu,
|
||||||
|
deletePart,
|
||||||
|
loadTab,
|
||||||
|
navigate,
|
||||||
|
openDetailAction
|
||||||
|
} from './helpers';
|
||||||
import { doCachedLogin } from './login';
|
import { doCachedLogin } from './login';
|
||||||
|
import { setSettingState } from './settings';
|
||||||
|
|
||||||
// Test hover form action in related fields
|
// Test hover form action in related fields
|
||||||
test('Forms - Hover', async ({ browser }) => {
|
test('Forms - Hover', async ({ browser }) => {
|
||||||
@@ -315,3 +322,72 @@ test('Forms - Nested Object Field', async ({ browser }) => {
|
|||||||
// Confirm the part was created with the expected name
|
// Confirm the part was created with the expected name
|
||||||
await page.getByText(partName).first().waitFor();
|
await page.getByText(partName).first().waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Regression for #12266: initial stock fields respect PART_CREATE_INITIAL setting
|
||||||
|
test('Forms - Initial Stock Field', async ({ browser }) => {
|
||||||
|
await setSettingState({ setting: 'PART_CREATE_INITIAL', value: true });
|
||||||
|
|
||||||
|
const partName = `Initial Stock Part ${Date.now()}`;
|
||||||
|
await deletePart(partName);
|
||||||
|
|
||||||
|
const page = await doCachedLogin(browser, {
|
||||||
|
user: stevenuser,
|
||||||
|
url: 'part/category/index/parts'
|
||||||
|
});
|
||||||
|
await page.waitForURL('**/part/category/index/**');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'action-menu-add-parts' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'action-menu-add-parts-create-part' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page.getByLabel('text-field-name', { exact: true }).fill(partName);
|
||||||
|
|
||||||
|
const quantityField = page.getByLabel('number-field-initial_stock.quantity');
|
||||||
|
await expect(quantityField).toBeVisible();
|
||||||
|
|
||||||
|
await quantityField.fill('25');
|
||||||
|
await page.getByLabel('tree-field-initial_stock.location').fill('production');
|
||||||
|
await page.getByText('Electronics production facility').click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Item Created').waitFor();
|
||||||
|
|
||||||
|
await page.getByText(partName).first().click();
|
||||||
|
await page
|
||||||
|
.getByLabel('panel-tabs-part')
|
||||||
|
.getByRole('tab', { name: 'Stock', exact: true })
|
||||||
|
.click();
|
||||||
|
await page.getByText('25').first().waitFor();
|
||||||
|
|
||||||
|
await deletePart(partName);
|
||||||
|
await setSettingState({ setting: 'PART_CREATE_INITIAL', value: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Forms - Initial Stock hidden when setting disabled', async ({
|
||||||
|
browser
|
||||||
|
}) => {
|
||||||
|
await setSettingState({ setting: 'PART_CREATE_INITIAL', value: false });
|
||||||
|
|
||||||
|
const page = await doCachedLogin(browser, {
|
||||||
|
user: stevenuser,
|
||||||
|
url: 'part/category/index/parts'
|
||||||
|
});
|
||||||
|
await page.waitForURL('**/part/category/index/**');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'action-menu-add-parts' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'action-menu-add-parts-create-part' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByLabel('text-field-name', { exact: true })
|
||||||
|
.fill('No Initial Stock');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByLabel('number-field-initial_stock.quantity')
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(
|
||||||
|
page.getByLabel('tree-field-initial_stock.location')
|
||||||
|
).toBeHidden();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user