2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-16 17:28:11 +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

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