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:
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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 %}
|
||||||
|
114
InvenTree/company/test_supplier_parts.py
Normal file
114
InvenTree/company/test_supplier_parts.py
Normal 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()
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
Reference in New Issue
Block a user