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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user