2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-15 16:58:14 +00:00

Merge branch 'generic-parameters' of https://github.com/schrodingersgat/inventree into pr/SchrodingersGat/10699

This commit is contained in:
Matthias Mair
2025-11-26 21:01:26 +01:00
14 changed files with 169 additions and 100 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,18 @@
---
title: Attachments
---
## Attachments
An *attachment* is a file which has been uploaded and linked to a specific object within InvenTree. Attachments can be used to store additional documentation, images, or other relevant files associated with various InvenTree models.
!!! note "Business Logic"
Attachments are not used for any core business logic within InvenTree. They are intended to provide additional metadata for objects, which can be useful for documentation, reference, or reporting purposes.
Parameters can be associated with various InvenTree models.
### Attachments Tab
Any model which supports attachments will have an "Attachments" tab on its detail page. This tab displays all attachments associated with that object:
{{ image("concepts/attachment-tab.png", "Order Attachments Example") }}

View File

@@ -1,17 +1,21 @@
---
title: Part Parameters
title: Parameters
---
## Part Parameters
## Parameters
A part *parameter* describes a particular "attribute" or "property" of a specific part.
A *parameter* describes a particular "attribute" or "property" of a specific object in InvenTree. Parameters allow for flexible and customizable data to be stored against various InvenTree models.
Part parameters are located in the "Parameters" tab, on each part detail page.
There is no limit for the number of part parameters and they are fully customizable through the use of [parameter templates](#parameter-templates).
!!! note "Business Logic"
Parameters are not used for any core business logic within InvenTree. They are intended to provide additional metadata for objects, which can be useful for documentation, filtering, or reporting purposes.
Here is an example of parameters for a capacitor:
Parameters can be associated with various InvenTree models.
{{ image("part/part_parameters_example.png", "Part Parameters Example") }}
### Parameter Tab
Any model which supports parameters will have a "Parameters" tab on its detail page. This tab displays all parameters associated with that object:
{{ image("concepts/parameter-tab.png", "Part Parameters Example") }}
## Parameter Templates
@@ -22,13 +26,16 @@ Parameter templates are used to define the different types of parameters which a
| Name | The name of the parameter template (*must be unique*) |
| Description | Optional description for the template |
| Units | Optional units field (*must be a valid [physical unit](#parameter-units)*) |
| Model Type | The InvenTree model to which this parameter template applies (e.g. Part, Company, etc). If this is left blank, the template can be used for any model type. |
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
| Selection List | If set, parameters linked to this template can only be assigned values from the linked [selection list](#selection-lists) |
{{ image("concepts/parameter-template.png", "Parameters Template") }}
### Create Template
Parameter templates are created and edited via the [settings interface](../settings/global.md).
Parameter templates are created and edited via the [admin interface](../settings/admin.md).
To create a template:
@@ -54,11 +61,11 @@ Select the parameter `Template` you would like to use for this parameter, fill-o
## Parametric Tables
Parametric tables gather all parameters from all parts inside a particular [part category](./index.md#part-category) to be sorted and filtered.
Parametric tables gather all parameters from all objects of a particular type, to be sorted and filtered.
To access a category's parametric table, click on the "Parameters" tab within the category view:
Tables views which support parametric filtering and sorting will have a "Parametric View" button above the table:
{{ image("part/parametric_table_tab.png", "Parametric Table Tab") }}
{{ image("common/parametric-parts.png", "Parametric Parts Table") }}
### Sorting by Parameter Value
@@ -139,7 +146,7 @@ Parameter sorting takes unit conversion into account, meaning that values provid
### Selection Lists
Selection Lists can be used to add a large number of predefined values to a parameter template. This can be useful for parameters which must be selected from a large predefined list of values (e.g. a list of standardised colo codes). Choices on templates are limited to 5000 characters, selection lists can be used to overcome this limitation.
Selection Lists can be used to add a large number of predefined values to a parameter template. This can be useful for parameters which must be selected from a large predefined list of values (e.g. a list of standardized color codes). Choices on templates are limited to 5000 characters, selection lists can be used to overcome this limitation.
It is possible that plugins lock selection lists to ensure a known state.

View File

@@ -98,7 +98,7 @@ Sometimes, users may encounter unexpected error messages when updating their Inv
The most common problem here is that the correct sequence of steps has not been followed:
1. Ensure that the InvenTree web server and background worker processes are *halted*
1. Ensure that the InvenTree [web server](./start/processes.md#web-server) and [background worker](./start/processes.md#background-worker) processes are *halted*
1. Update the InvenTree software (e.g. using git or docker, depending on installation method)
1. Run the `invoke update` command
1. Restart the web server and background worker processes
@@ -150,7 +150,7 @@ or
### Background Worker "Not Running"
The background worker process must be started separately to the web-server application.
The [background worker process](./start/processes.md#background-worker) must be started separately to the web-server application.
From the top-level source directory, run the following command from a separate terminal, while the server is already running:

View File

@@ -123,11 +123,14 @@ By default, a production InvenTree installation is configured to run with [DEBUG
Running in DEBUG mode provides many handy development features, however it is strongly recommended *NOT* to run in DEBUG mode in a production environment. This recommendation is made because DEBUG mode leaks a lot of information about your installation and may pose a security risk.
So, for a production setup, you should set `INVENTREE_DEBUG=false` in the [configuration options](./config.md).
So, for a production setup, you should ensure that `INVENTREE_DEBUG=false` in the [configuration options](./config.md).
!!! warning "Security Risk"
Running InvenTree in DEBUG mode in a production environment is a significant security risk, and should be avoided at all costs.
### Turning Debug Mode off
When running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled*, the proxy setup has to be configured to handle this.
When running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled* (which is the default for a production setup), the proxy setup has to be configured to handle this.
!!! info "Read More"
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details

View File

@@ -9,6 +9,12 @@ If you are struggling with an issue which is not covered in the FAQ above, pleas
Even if you cannot immediately resolve the issue, the information below will be very useful when reporting the issue on GitHub.
## Error Codes
InvenTree uses a system of error codes to help identify specific issues. Each error code is prefixed with `INVE-`, followed by a letter indicating the error type, and a number.
Refer to the [error code documentation](./settings/error_codes.md) for more information on specific error codes.
## Recent Update
If you have recently updated your InvenTree instance, please ensure that you have followed all update instructions carefully. In particular, make sure that you have run any required database migrations using the `invoke update` command.

View File

@@ -96,6 +96,8 @@ nav:
- Custom States: concepts/custom_states.md
- Pricing: concepts/pricing.md
- Project Codes: concepts/project_codes.md
- Attachments: concepts/attachments.md
- Parameters: concepts/parameters.md
- Barcodes:
- Barcode Support: barcodes/index.md
- Internal Barcodes: barcodes/internal.md
@@ -125,7 +127,6 @@ nav:
- Virtual Parts: part/virtual.md
- Part Views: part/views.md
- Tracking: part/trackable.md
- Parameters: part/parameter.md
- Revisions: part/revision.md
- Templates: part/template.md
- Tests: part/test.md

View File

@@ -370,6 +370,86 @@ class PartManager(TreeManager):
)
class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
"""A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a ParameterTemplate.
Multiple ParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation.
Attributes:
category: Reference to a single PartCategory object
template: Reference to a single ParameterTemplate object
default_value: The default value for the parameter in the context of the selected category
"""
@staticmethod
def get_api_url():
"""Return the API endpoint URL associated with the PartCategoryParameterTemplate model."""
return reverse('api-part-category-parameter-list')
class Meta:
"""Metaclass providing extra model definition."""
verbose_name = _('Part Category Parameter Template')
constraints = [
UniqueConstraint(
fields=['category', 'template'], name='unique_category_parameter_pair'
)
]
def __str__(self):
"""String representation of a PartCategoryParameterTemplate (admin interface)."""
if self.default_value:
return f'{self.category.name} | {self.template.name} | {self.default_value}'
return f'{self.category.name} | {self.template.name}'
def clean(self):
"""Validate this PartCategoryParameterTemplate instance.
Checks the provided 'default_value', and (if not blank), ensure it is valid.
"""
super().clean()
self.default_value = (
'' if self.default_value is None else str(self.default_value.strip())
)
if (
self.default_value
and get_global_setting(
'PARAMETER_ENFORCE_UNITS', True, cache=False, create=False
)
and self.template.units
):
try:
InvenTree.conversion.convert_physical_value(
self.default_value, self.template.units
)
except ValidationError as e:
raise ValidationError({'default_value': e.message})
category = models.ForeignKey(
PartCategory,
on_delete=models.CASCADE,
related_name='parameter_templates',
verbose_name=_('Category'),
help_text=_('Part Category'),
)
template = models.ForeignKey(
common.models.ParameterTemplate,
on_delete=models.CASCADE,
related_name='part_categories',
)
default_value = models.CharField(
max_length=500,
blank=True,
verbose_name=_('Default Value'),
help_text=_('Default Parameter Value'),
)
class PartReportContext(report.mixins.BaseReportContext):
"""Report context for the Part model.
@@ -3714,86 +3794,6 @@ class PartTestTemplate(InvenTree.models.InvenTreeMetadataModel):
return [x.strip() for x in self.choices.split(',') if x.strip()]
class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
"""A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a ParameterTemplate.
Multiple ParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation.
Attributes:
category: Reference to a single PartCategory object
template: Reference to a single ParameterTemplate object
default_value: The default value for the parameter in the context of the selected category
"""
@staticmethod
def get_api_url():
"""Return the API endpoint URL associated with the PartCategoryParameterTemplate model."""
return reverse('api-part-category-parameter-list')
class Meta:
"""Metaclass providing extra model definition."""
verbose_name = _('Part Category Parameter Template')
constraints = [
UniqueConstraint(
fields=['category', 'template'], name='unique_category_parameter_pair'
)
]
def __str__(self):
"""String representation of a PartCategoryParameterTemplate (admin interface)."""
if self.default_value:
return f'{self.category.name} | {self.template.name} | {self.default_value}'
return f'{self.category.name} | {self.template.name}'
def clean(self):
"""Validate this PartCategoryParameterTemplate instance.
Checks the provided 'default_value', and (if not blank), ensure it is valid.
"""
super().clean()
self.default_value = (
'' if self.default_value is None else str(self.default_value.strip())
)
if (
self.default_value
and get_global_setting(
'PARAMETER_ENFORCE_UNITS', True, cache=False, create=False
)
and self.template.units
):
try:
InvenTree.conversion.convert_physical_value(
self.default_value, self.template.units
)
except ValidationError as e:
raise ValidationError({'default_value': e.message})
category = models.ForeignKey(
PartCategory,
on_delete=models.CASCADE,
related_name='parameter_templates',
verbose_name=_('Category'),
help_text=_('Part Category'),
)
template = models.ForeignKey(
common.models.ParameterTemplate,
on_delete=models.CASCADE,
related_name='part_categories',
)
default_value = models.CharField(
max_length=500,
blank=True,
verbose_name=_('Default Value'),
help_text=_('Default Parameter Value'),
)
class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
"""A BomItem links a part to its component items.

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

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

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