2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 11:35:41 +00:00

Clean pack units when saving a SupplierPart

- Convert to native part units
- Handle empty units value
- Add unit tests
This commit is contained in:
Oliver Walters
2023-05-22 00:52:48 +10:00
parent 5226f1e1ff
commit c078831a35
6 changed files with 182 additions and 24 deletions

View File

@ -43,7 +43,7 @@ def convert_physical_value(value: str, unit: str = None):
unit: Optional unit to convert to, and validate against
Raises:
ValidationError: If the value is invalid
ValidationError: If the value is invalid or cannot be converted to the specified unit
Returns:
The converted quantity, in the specified units

View File

@ -2,6 +2,7 @@
import os
from datetime import datetime
from decimal import Decimal
from django.apps import apps
from django.core.exceptions import ValidationError
@ -477,10 +478,35 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
"""
super().clean()
self.pack_units = self.pack_units.strip()
# An empty 'pack_units' value is equivalent to '1'
if self.pack_units == '':
self.pack_units = '1'
# Validate that the UOM is compatible with the base part
if self.pack_units and self.part:
try:
InvenTree.conversion.convert_physical_value(self.pack_units, self.part.units)
# Attempt conversion to specified unit
native_value = InvenTree.conversion.convert_physical_value(
self.pack_units, self.part.units
)
# If part units are not provided, value must be dimensionless
if not self.part.units and native_value.units not in ['', 'dimensionless']:
raise ValidationError({
'pack_units': _("Pack units must be compatible with the base part units")
})
# Native value must be greater than zero
if float(native_value.magnitude) <= 0:
raise ValidationError({
'pack_units': _("Pack units must be greater than zero")
})
# Update native pack units value
self.pack_units_native = Decimal(native_value.magnitude)
except ValidationError as e:
raise ValidationError({
'pack_units': e.messages
@ -521,21 +547,23 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
super().save(*args, **kwargs)
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='supplier_parts',
verbose_name=_('Base Part'),
limit_choices_to={
'purchaseable': True,
},
help_text=_('Select part'),
)
part = models.ForeignKey(
'part.Part', on_delete=models.CASCADE,
related_name='supplier_parts',
verbose_name=_('Base Part'),
limit_choices_to={
'purchaseable': True,
},
help_text=_('Select part'),
)
supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
related_name='supplied_parts',
limit_choices_to={'is_supplier': True},
verbose_name=_('Supplier'),
help_text=_('Select supplier'),
)
supplier = models.ForeignKey(
Company, on_delete=models.CASCADE,
related_name='supplied_parts',
limit_choices_to={'is_supplier': True},
verbose_name=_('Supplier'),
help_text=_('Select supplier'),
)
SKU = models.CharField(
max_length=100,
@ -543,12 +571,13 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
help_text=_('Supplier stock keeping unit')
)
manufacturer_part = models.ForeignKey(ManufacturerPart, on_delete=models.CASCADE,
blank=True, null=True,
related_name='supplier_parts',
verbose_name=_('Manufacturer Part'),
help_text=_('Select manufacturer part'),
)
manufacturer_part = models.ForeignKey(
ManufacturerPart, on_delete=models.CASCADE,
blank=True, null=True,
related_name='supplier_parts',
verbose_name=_('Manufacturer Part'),
help_text=_('Select manufacturer part'),
)
link = InvenTreeURLField(
blank=True, null=True,

View File

@ -165,8 +165,15 @@ src="{% static 'img/blank_image.png' %}"
{% if part.pack_units %}
<tr>
<td><span class='fas fa-box'></span></td>
<td>{% trans "Units" %}</td>
<td>{% part.pack_units %} {% include "part/part_units.html" with part=part.part %}</td>
<td>
{% trans "Units" %}
{% if part.part.units %}
<span class='float-right'>
<em>[ {% include "part/part_units.html" with part=part.part %}]</em>
</span>
{% endif %}
</td>
<td>{{ part.pack_units }}</td>
</tr>
{% endif %}
{% if part.note %}

View File

@ -0,0 +1,114 @@
"""Unit tests specific to the SupplierPart model"""
from decimal import Decimal
from django.core.exceptions import ValidationError
from company.models import Company, SupplierPart
from InvenTree.unit_test import InvenTreeTestCase
from part.models import Part
class SupplierPartPackUnitsTests(InvenTreeTestCase):
"""Unit tests for the SupplierPart pack_units field"""
def test_pack_units_dimensionless(self):
"""Test valid values for the 'pack_units' field"""
# Create a part without units (dimensionless)
part = Part.objects.create(name='Test Part', description='Test part description', component=True)
# Create a supplier (company)
company = Company.objects.create(name='Test Company', is_supplier=True)
# Create a supplier part for this part
sp = SupplierPart.objects.create(
part=part,
supplier=company,
SKU='TEST-SKU'
)
# All these values are valid for a dimensionless part
pass_tests = {
'': 1,
'1': 1,
'1.01': 1.01,
'12.000001': 12.000001,
'99.99': 99.99,
}
# All these values are invalid for a dimensionless part
fail_tests = [
'1.2m',
'-1',
'0',
'0.0',
'100 feet',
'0 amps'
]
for test, expected in pass_tests.items():
sp.pack_units = test
sp.full_clean()
self.assertEqual(sp.pack_units_native, expected)
for test in fail_tests:
sp.pack_units = test
with self.assertRaises(ValidationError):
sp.full_clean()
def test_pack_units(self):
"""Test pack_units for a part with a specified dimension"""
# Create a part with units 'm'
part = Part.objects.create(name='Test Part', description='Test part description', component=True, units='m')
# Create a supplier (company)
company = Company.objects.create(name='Test Company', is_supplier=True)
# Create a supplier part for this part
sp = SupplierPart.objects.create(
part=part,
supplier=company,
SKU='TEST-SKU'
)
# All these values are valid for a part with dimesion 'm'
pass_tests = {
'': 1,
'1': 1,
'1m': 1,
'1.01m': 1.01,
'1.01': 1.01,
'5 inches': 0.127,
'27 cm': 0.27,
'3km': 3000,
'14 feet': 4.2672,
'0.5 miles': 804.672,
}
# All these values are invalid for a part with dimension 'm'
# Either the values are invalid, or the units are incomaptible
fail_tests = [
'-1',
'-1m',
'0',
'0m',
'12 deg',
'57 amps',
'-12 oz',
'17 yaks',
]
for test, expected in pass_tests.items():
sp.pack_units = test
sp.full_clean()
self.assertEqual(
round(Decimal(sp.pack_units_native), 10),
round(Decimal(str(expected)), 10)
)
for test in fail_tests:
sp.pack_units = test
with self.assertRaises(ValidationError):
sp.full_clean()

View File

@ -19,6 +19,10 @@ def update_template_units(apps, schema_editor):
n_templates = PartParameterTemplate.objects.count()
if n_templates == 0:
# Escape early
return
ureg = InvenTree.conversion.get_unit_registry()
n_converted = 0

View File

@ -20,6 +20,10 @@ def migrate_part_units(apps, schema_editor):
parts = Part.objects.exclude(units=None).exclude(units='')
n_parts = parts.count()
if n_parts == 0:
# Escape early
return
ureg = InvenTree.conversion.get_unit_registry()
invalid_units = set()