2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 03:55: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 unit: Optional unit to convert to, and validate against
Raises: Raises:
ValidationError: If the value is invalid ValidationError: If the value is invalid or cannot be converted to the specified unit
Returns: Returns:
The converted quantity, in the specified units The converted quantity, in the specified units

View File

@ -2,6 +2,7 @@
import os import os
from datetime import datetime from datetime import datetime
from decimal import Decimal
from django.apps import apps from django.apps import apps
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -477,10 +478,35 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
""" """
super().clean() 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 # Validate that the UOM is compatible with the base part
if self.pack_units and self.part: if self.pack_units and self.part:
try: 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: except ValidationError as e:
raise ValidationError({ raise ValidationError({
'pack_units': e.messages 'pack_units': e.messages
@ -521,21 +547,23 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
super().save(*args, **kwargs) super().save(*args, **kwargs)
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, part = models.ForeignKey(
related_name='supplier_parts', 'part.Part', on_delete=models.CASCADE,
verbose_name=_('Base Part'), related_name='supplier_parts',
limit_choices_to={ verbose_name=_('Base Part'),
'purchaseable': True, limit_choices_to={
}, 'purchaseable': True,
help_text=_('Select part'), },
) help_text=_('Select part'),
)
supplier = models.ForeignKey(Company, on_delete=models.CASCADE, supplier = models.ForeignKey(
related_name='supplied_parts', Company, on_delete=models.CASCADE,
limit_choices_to={'is_supplier': True}, related_name='supplied_parts',
verbose_name=_('Supplier'), limit_choices_to={'is_supplier': True},
help_text=_('Select supplier'), verbose_name=_('Supplier'),
) help_text=_('Select supplier'),
)
SKU = models.CharField( SKU = models.CharField(
max_length=100, max_length=100,
@ -543,12 +571,13 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
help_text=_('Supplier stock keeping unit') help_text=_('Supplier stock keeping unit')
) )
manufacturer_part = models.ForeignKey(ManufacturerPart, on_delete=models.CASCADE, manufacturer_part = models.ForeignKey(
blank=True, null=True, ManufacturerPart, on_delete=models.CASCADE,
related_name='supplier_parts', blank=True, null=True,
verbose_name=_('Manufacturer Part'), related_name='supplier_parts',
help_text=_('Select manufacturer part'), verbose_name=_('Manufacturer Part'),
) help_text=_('Select manufacturer part'),
)
link = InvenTreeURLField( link = InvenTreeURLField(
blank=True, null=True, blank=True, null=True,

View File

@ -165,8 +165,15 @@ src="{% static 'img/blank_image.png' %}"
{% if part.pack_units %} {% if part.pack_units %}
<tr> <tr>
<td><span class='fas fa-box'></span></td> <td><span class='fas fa-box'></span></td>
<td>{% trans "Units" %}</td> <td>
<td>{% part.pack_units %} {% include "part/part_units.html" with part=part.part %}</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> </tr>
{% endif %} {% endif %}
{% if part.note %} {% 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() n_templates = PartParameterTemplate.objects.count()
if n_templates == 0:
# Escape early
return
ureg = InvenTree.conversion.get_unit_registry() ureg = InvenTree.conversion.get_unit_registry()
n_converted = 0 n_converted = 0

View File

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