2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 04:26:44 +00:00

Update validation "rules" for BuildItem

- A BuildItem which points to a trackable part must also point to a build output
- A BuildItem which points to a non-trackable part cannot point to a build output
This commit is contained in:
Oliver Walters 2020-10-26 08:34:17 +11:00
parent 6aaf178f0b
commit ffe15763a7
9 changed files with 106 additions and 141 deletions

View File

@ -1,25 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-20 09:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0052_stockitem_is_building'),
('build', '0020_auto_20201019_1325'),
]
operations = [
migrations.AddField(
model_name='builditem',
name='install_into',
field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem'),
),
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'),
),
]

View File

@ -0,0 +1,64 @@
# Generated by Django 3.0.7 on 2020-10-25 21:33
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
replaces = [('build', '0021_auto_20201020_0908'), ('build', '0022_auto_20201020_0953'), ('build', '0023_auto_20201020_1009'), ('build', '0024_auto_20201020_1144'), ('build', '0025_auto_20201020_1248'), ('build', '0026_auto_20201023_1228')]
dependencies = [
('stock', '0052_stockitem_is_building'),
('build', '0020_auto_20201019_1325'),
('part', '0051_bomitem_optional'),
]
operations = [
migrations.AddField(
model_name='builditem',
name='install_into',
field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem'),
),
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'),
),
migrations.AddField(
model_name='build',
name='destination',
field=models.ForeignKey(blank=True, help_text='Select location where the completed items will be stored', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_builds', to='stock.StockLocation', verbose_name='Destination Location'),
),
migrations.AlterField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, help_text='BuildOrder to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'),
),
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Production'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'),
),
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'),
),
migrations.AddField(
model_name='build',
name='completed',
field=models.PositiveIntegerField(default=0, help_text='Number of stock items which have been completed', verbose_name='Completed items'),
),
migrations.AlterField(
model_name='build',
name='quantity',
field=models.PositiveIntegerField(default=1, help_text='Number of stock items to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'),
),
migrations.AlterUniqueTogether(
name='builditem',
unique_together={('build', 'stock_item', 'install_into')},
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-20 09:53
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('stock', '0052_stockitem_is_building'),
('build', '0021_auto_20201020_0908'),
]
operations = [
migrations.AddField(
model_name='build',
name='destination',
field=models.ForeignKey(blank=True, help_text='Select location where the completed items will be stored', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_builds', to='stock.StockLocation', verbose_name='Destination Location'),
),
migrations.AlterField(
model_name='build',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, help_text='BuildOrder to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-20 10:09
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0022_auto_20201020_0953'),
]
operations = [
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Production'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-20 11:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0051_bomitem_optional'),
('build', '0023_auto_20201020_1009'),
]
operations = [
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-20 12:48
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0024_auto_20201020_1144'),
]
operations = [
migrations.AddField(
model_name='build',
name='completed',
field=models.PositiveIntegerField(default=0, help_text='Number of stock items which have been completed', verbose_name='Completed items'),
),
migrations.AlterField(
model_name='build',
name='quantity',
field=models.PositiveIntegerField(default=1, help_text='Number of stock items to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.0.7 on 2020-10-23 12:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0052_stockitem_is_building'),
('build', '0025_auto_20201020_1248'),
]
operations = [
migrations.AlterUniqueTogether(
name='builditem',
unique_together={('build', 'stock_item', 'install_into')},
),
]

View File

@ -670,6 +670,28 @@ class BuildItem(models.Model):
('build', 'stock_item', 'install_into'), ('build', 'stock_item', 'install_into'),
] ]
def validate_unique(self, exclude=None):
"""
Test that this BuildItem object is "unique".
Essentially we do not want a stock_item being allocated to a Build multiple times.
"""
super().validate_unique(exclude)
items = BuildItem.objects.exclude(id=self.id).filter(
build=self.build,
stock_item=self.stock_item,
install_into=self.install_into
)
if items.exists():
msg = _("BuildItem must be unique for build, stock_item and install_into")
raise ValidationError({
'build': msg,
'stock_item': msg,
'install_into': msg
})
def clean(self): def clean(self):
""" Check validity of the BuildItem model. """ Check validity of the BuildItem model.
The following checks are performed: The following checks are performed:
@ -678,7 +700,9 @@ class BuildItem(models.Model):
- Allocation quantity cannot exceed available quantity - Allocation quantity cannot exceed available quantity
""" """
super(BuildItem, self).clean() self.validate_unique()
super().clean()
errors = {} errors = {}
@ -711,6 +735,14 @@ class BuildItem(models.Model):
if not self.install_into.part == self.build.part: if not self.install_into.part == self.build.part:
errors['install_into'] = _('Part reference differs between build and build output') errors['install_into'] = _('Part reference differs between build and build output')
# A trackable StockItem *must* point to a build output
if self.stock_item.part.trackable and self.install_into is None:
errors['install_into'] = _('Trackable BuildItem must reference a build output')
# A non-trackable StockItem *must not* point to a build output
if not self.stock_item.part.trackable and self.install_into is not None:
errors['install_into'] = _('Non-trackable BuildItem must not reference a build output')
except (StockModels.StockItem.DoesNotExist, PartModels.Part.DoesNotExist): except (StockModels.StockItem.DoesNotExist, PartModels.Part.DoesNotExist):
pass pass

View File

@ -3,7 +3,6 @@
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from build.models import Build, BuildItem from build.models import Build, BuildItem
@ -144,14 +143,16 @@ class BuildTest(TestCase):
quantity=q21 quantity=q21
) )
with transaction.atomic(): # Attempt to create another identical BuildItem
with self.assertRaises(IntegrityError): b = BuildItem(
BuildItem.objects.create(
build=self.build, build=self.build,
stock_item=self.stock_2_1, stock_item=self.stock_2_1,
quantity=99 quantity=q21
) )
with self.assertRaises(ValidationError):
b.clean()
self.assertEqual(BuildItem.objects.count(), 3) self.assertEqual(BuildItem.objects.count(), 3)
def test_partial_allocation(self): def test_partial_allocation(self):