Compare commits

..

12 Commits
1.1.5 ... 1.1.x

Author SHA1 Message Date
github-actions[bot]
47f386e39a change screenshot for plugin install to PUI (#11036) (#11037)
* change screenshot for plugin install to PUI

* Correct location of gunicorn config in docs

(cherry picked from commit 00091caf04)

Co-authored-by: Michael <michael@buchmann.ruhr>
2025-12-18 10:51:53 +11:00
Oliver
700d49643d Bump InvenTree software version to 1.1.8 (#11028) 2025-12-17 08:45:11 +11:00
Oliver
2f9cf5f1f1 Default Supplier Support Missing in 1.X.X (#10980) (#11027)
Fixes #10979

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-12-17 08:44:27 +11:00
github-actions[bot]
3eb6f12570 Fix for string form fields (#10814) (#10968)
* Fix for string form fields

- replace null values with empty strings

* Expose more serializer metadata

* Check if null values are not allowed

* Fix type

* Try removing feature

* Reduce deltas

* Remove extra field attrs entirely (for testing)

* Comment out changes

* Tweak form values

* Fix for form validation

(cherry picked from commit efc8fb816d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-12-07 00:14:58 +11:00
github-actions[bot]
afc1dad8a7 Obvserve default values for part forms (#10964) (#10965)
- Closes https://github.com/inventree/InvenTree/issues/10909
- Use global setting values as defaults

(cherry picked from commit 3a18934b83)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-12-06 20:28:22 +11:00
github-actions[bot]
66b71c1f2e Fixed typo in shebang interpreter directive (#10952) (#10953)
(cherry picked from commit 2ffc2cb9fc)

Co-authored-by: Tyler Tracy <tylertracy@gmail.com>
2025-12-04 12:10:09 +11:00
github-actions[bot]
1a8287824b Allow null values for InvenTreeDecimalField (#10948) (#10951)
- Fixes bug related to importing null "rounding_multiple" BOM field

(cherry picked from commit 7920b0e670)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-12-04 07:15:38 +11:00
github-actions[bot]
10769ccb04 [bug] Handle TransactionManagementError (#10942) (#10943)
In the case where we try to call refresh_from_db within an atomic transaction block, it will throw a TransactionManagementError

(cherry picked from commit 38b27271ac)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-12-02 17:23:09 +11:00
Oliver
f39b3190e3 Bump software version to 1.1.7 (#10916) 2025-11-26 23:40:40 +11:00
github-actions[bot]
e1a97b2a39 [bug] Stock adjust (#10914) (#10915)
* Extra checks on backend

* Bug fix for adjustment forms

- Set default quantity of zero

* Additional unit testing (to ensure no regression)

(cherry picked from commit 5713cff1cb)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-11-26 22:41:36 +11:00
github-actions[bot]
c9a1d9adda Installer missing some required packages from REQS (#10897) (#10898)
Fixes #10813

(cherry picked from commit fcea1383d0)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-11-24 09:22:53 +11:00
Oliver
64fb5c062a Bump software version to 1.1.6 (#10890) 2025-11-22 22:18:41 +11:00
16 changed files with 145 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
#!/bin/ash
#!/bin/bash
# exit when any command fails
set -e

View File

@@ -15,7 +15,7 @@ root_command() {
no_call=${args[--no-call]}
dry_run=${args[--dry-run]}
REQS="wget apt-transport-https"
REQS="wget apt-transport-https curl gpg"
function do_call() {
if [[ $dry_run ]]; then

View File

@@ -5,7 +5,7 @@ publisher=${args[publisher]}
no_call=${args[--no-call]}
dry_run=${args[--dry-run]}
REQS="wget apt-transport-https"
REQS="wget apt-transport-https curl gpg"
function do_call() {
if [[ $dry_run ]]; then

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -38,7 +38,7 @@ First, let's confirm that the gunicorn server is operational.
cd /home/InvenTree
source ./env/bin/activate
cd src/InvenTree
cd src/src/backend/InvenTree
/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi -b 127.0.0.1:8000
```

View File

@@ -420,6 +420,13 @@ class InvenTreeMetadata(SimpleMetadata):
if field_info['type'] == 'dependent field':
field_info['depends_on'] = field.depends_on
# Extends with extra attributes from the serializer
extra_field_attributes = ['allow_blank', 'allow_null']
for attr in extra_field_attributes:
if hasattr(field, attr):
field_info[attr] = getattr(field, attr)
# Extend field info if the field has a get_field_info method
if (
not field_info.get('read_only')

View File

@@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import QuerySet
from django.db.models.signals import post_save
from django.db.transaction import TransactionManagementError
from django.dispatch import receiver
from django.urls import resolve, reverse
from django.urls.exceptions import NoReverseMatch
@@ -757,7 +758,15 @@ class InvenTreeTree(MPTTModel):
if len(trees) > 0:
# A tree update was performed, so we need to refresh the instance
self.refresh_from_db()
try:
self.refresh_from_db()
except TransactionManagementError:
# If we are inside a transaction block, we cannot refresh from db
pass
except Exception as e:
# Any other error is unexpected
InvenTree.sentry.report_exception(e)
InvenTree.exceptions.log_error(f'{self.__class__.__name__}.save')
def partial_rebuild(self, tree_id: int) -> bool:
"""Perform a partial rebuild of the tree structure.

View File

@@ -646,6 +646,11 @@ class InvenTreeDecimalField(serializers.FloatField):
def to_internal_value(self, data):
"""Convert to python type."""
if data in [None, '']:
if self.allow_null:
return None
raise serializers.ValidationError(_('This field may not be null.'))
# Convert the value to a string, and then a decimal
try:
return Decimal(str(data))

View File

@@ -18,7 +18,7 @@ from django.conf import settings
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = '1.1.5'
INVENTREE_SW_VERSION = '1.1.8'
logger = logging.getLogger('inventree')

View File

@@ -1678,6 +1678,10 @@ class StockAddSerializer(StockAdjustmentSerializer):
stock_item = item['pk']
quantity = item['quantity']
if quantity is None or quantity <= 0:
# Ignore in this case - no stock to add
continue
# Optional fields
extra = {}
@@ -1703,6 +1707,10 @@ class StockRemoveSerializer(StockAdjustmentSerializer):
stock_item = item['pk']
quantity = item['quantity']
# Ignore in this case - no stock to remove
if quantity is None or quantity <= 0:
continue
# Optional fields
extra = {}

View File

@@ -48,6 +48,8 @@ export type ApiFormFieldHeader = {
* @param model : The model to use for related fields
* @param filters : Optional API filters to apply to related fields
* @param required : Whether the field is required
* @param allow_null: Whether the field allows null values
* @param allow_blank: Whether the field allows blank values
* @param hidden : Whether the field is hidden
* @param disabled : Whether the field is disabled
* @param error : Optional error message to display
@@ -103,6 +105,8 @@ export type ApiFormFieldType = {
choices?: ApiFormFieldChoice[];
hidden?: boolean;
disabled?: boolean;
allow_null?: boolean;
allow_blank?: boolean;
exclude?: boolean;
read_only?: boolean;
placeholder?: string;

View File

@@ -24,7 +24,11 @@ import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { isTrue } from '@lib/functions/Conversion';
import { getDetailUrl } from '@lib/functions/Navigation';
import type { ApiFormFieldSet, ApiFormProps } from '@lib/types/Forms';
import type {
ApiFormFieldSet,
ApiFormFieldType,
ApiFormProps
} from '@lib/types/Forms';
import { useApi } from '../../contexts/ApiContext';
import {
type NestedDict,
@@ -375,16 +379,29 @@ export function ApiForm({
Object.keys(data).forEach((key: string) => {
let value: any = data[key];
const field_type = fields[key]?.field_type;
const exclude = fields[key]?.exclude;
const field: ApiFormFieldType = fields[key] ?? {};
const field_type = field?.field_type;
const exclude = field?.exclude;
if (field_type == 'file upload' && !!value) {
hasFiles = true;
}
// Ensure any boolean values are actually boolean
if (field_type === 'boolean') {
value = isTrue(value) || false;
// Special consideration for various field types
switch (field_type) {
case 'boolean':
// Ensure boolean values are actually boolean
value = isTrue(value) || false;
break;
case 'string':
// Replace null string values with an empty string
if (value === null && field?.allow_null == false) {
value = '';
jsonData[key] = value;
}
break;
default:
break;
}
// Stringify any JSON objects
@@ -393,7 +410,9 @@ export function ApiForm({
case 'file upload':
break;
default:
value = JSON.stringify(value);
if (value !== null && value !== undefined) {
value = JSON.stringify(value);
}
break;
}
}

View File

@@ -1,10 +1,11 @@
import { ModelType } from '@lib/index';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { t } from '@lingui/core/macro';
import { IconBuildingStore, IconCopy, IconPackages } from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { useApi } from '../contexts/ApiContext';
import { useGlobalSettingsState } from '../states/SettingsStates';
@@ -20,8 +21,12 @@ export function usePartFields({
}): ApiFormFieldSet {
const settings = useGlobalSettingsState();
const [virtual, setVirtual] = useState<boolean>(false);
const [purchaseable, setPurchaseable] = useState<boolean>(false);
const globalSettings = useGlobalSettingsState();
const [virtual, setVirtual] = useState<boolean | undefined>(undefined);
const [purchaseable, setPurchaseable] = useState<boolean | undefined>(
undefined
);
return useMemo(() => {
const fields: ApiFormFieldSet = {
@@ -53,6 +58,13 @@ export function usePartFields({
structural: false
}
},
default_supplier: {
model: ModelType.company,
api_url: apiUrl(ApiEndpoints.company_list),
filters: {
is_supplier: true
}
},
default_expiry: {},
minimum_stock: {},
responsible: {
@@ -60,19 +72,33 @@ export function usePartFields({
is_active: true
}
},
component: {},
assembly: {},
is_template: {},
testable: {},
trackable: {},
component: {
default: globalSettings.isSet('PART_COMPONENT')
},
assembly: {
default: globalSettings.isSet('PART_ASSEMBLY')
},
is_template: {
default: globalSettings.isSet('PART_TEMPLATE')
},
testable: {
default: false
},
trackable: {
default: globalSettings.isSet('PART_TRACKABLE')
},
purchaseable: {
value: purchaseable,
default: globalSettings.isSet('PART_PURCHASEABLE'),
onValueChange: (value: boolean) => {
setPurchaseable(value);
}
},
salable: {},
salable: {
default: globalSettings.isSet('PART_SALABLE')
},
virtual: {
default: globalSettings.isSet('PART_VIRTUAL'),
value: virtual,
onValueChange: (value: boolean) => {
setVirtual(value);
@@ -93,7 +119,7 @@ export function usePartFields({
if (create) {
fields.copy_category_parameters = {};
if (!virtual) {
if (virtual != false) {
fields.initial_stock = {
icon: <IconPackages />,
children: {
@@ -176,7 +202,14 @@ export function usePartFields({
}
return fields;
}, [virtual, purchaseable, create, duplicatePartInstance, settings]);
}, [
virtual,
purchaseable,
create,
globalSettings,
duplicatePartInstance,
settings
]);
}
/**

View File

@@ -862,10 +862,17 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet {
const records = Object.fromEntries(items.map((item) => [item.pk, item]));
const initialValue = mapAdjustmentItems(items).map((elem) => {
return {
...elem,
quantity: 0
};
});
const fields: ApiFormFieldSet = {
items: {
field_type: 'table',
value: mapAdjustmentItems(items),
value: initialValue,
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.pk];
@@ -902,10 +909,17 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
const records = Object.fromEntries(items.map((item) => [item.pk, item]));
const initialValue = mapAdjustmentItems(items).map((elem) => {
return {
...elem,
quantity: 0
};
});
const fields: ApiFormFieldSet = {
items: {
field_type: 'table',
value: mapAdjustmentItems(items),
value: initialValue,
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.pk];
@@ -941,10 +955,12 @@ function stockCountFields(items: any[]): ApiFormFieldSet {
const records = Object.fromEntries(items.map((item) => [item.pk, item]));
const initialValue = mapAdjustmentItems(items);
const fields: ApiFormFieldSet = {
items: {
field_type: 'table',
value: mapAdjustmentItems(items),
value: initialValue,
modelRenderer: (row: TableFieldRowProps) => {
return (
<StockOperationsRow

View File

@@ -501,6 +501,13 @@ export default function PartDetail() {
model: ModelType.stocklocation,
hidden: part.default_location || !part.category_default_location
},
{
type: 'link',
name: 'default_supplier',
label: t`Default Supplier`,
model: ModelType.company,
hidden: !part.default_supplier
},
{
type: 'string',
name: 'units',

View File

@@ -332,6 +332,11 @@ test('Stock - Stock Actions', async ({ browser }) => {
await page.getByRole('button', { name: 'Scan', exact: true }).click();
await page.getByText('Scanned stock item into location').waitFor();
// Add "zero" stock - ensure the quantity stays the same
await launchStockAction('add');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Quantity: 123').first().waitFor();
// Add stock, and change status
await launchStockAction('add');
await page.getByLabel('number-field-quantity').fill('12');
@@ -342,6 +347,11 @@ test('Stock - Stock Actions', async ({ browser }) => {
await page.getByText('Unavailable').first().waitFor();
await page.getByText('135').first().waitFor();
// Remove "zero" stock - ensure the quantity stays the same
await launchStockAction('remove');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Quantity: 135').first().waitFor();
// Remove stock, and change status
await launchStockAction('remove');
await page.getByLabel('number-field-quantity').fill('99');