From ffe15763a72df570ef4da26f1b5b1df47a57fc43 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 26 Oct 2020 08:34:17 +1100 Subject: [PATCH] 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 --- .../migrations/0021_auto_20201020_0908.py | 25 -------- ...0_0908_squashed_0026_auto_20201023_1228.py | 64 +++++++++++++++++++ .../migrations/0022_auto_20201020_0953.py | 26 -------- .../migrations/0023_auto_20201020_1009.py | 19 ------ .../migrations/0024_auto_20201020_1144.py | 20 ------ .../migrations/0025_auto_20201020_1248.py | 24 ------- .../migrations/0026_auto_20201023_1228.py | 18 ------ InvenTree/build/models.py | 34 +++++++++- InvenTree/build/test_build.py | 17 ++--- 9 files changed, 106 insertions(+), 141 deletions(-) delete mode 100644 InvenTree/build/migrations/0021_auto_20201020_0908.py create mode 100644 InvenTree/build/migrations/0021_auto_20201020_0908_squashed_0026_auto_20201023_1228.py delete mode 100644 InvenTree/build/migrations/0022_auto_20201020_0953.py delete mode 100644 InvenTree/build/migrations/0023_auto_20201020_1009.py delete mode 100644 InvenTree/build/migrations/0024_auto_20201020_1144.py delete mode 100644 InvenTree/build/migrations/0025_auto_20201020_1248.py delete mode 100644 InvenTree/build/migrations/0026_auto_20201023_1228.py diff --git a/InvenTree/build/migrations/0021_auto_20201020_0908.py b/InvenTree/build/migrations/0021_auto_20201020_0908.py deleted file mode 100644 index 5fa450f5c7..0000000000 --- a/InvenTree/build/migrations/0021_auto_20201020_0908.py +++ /dev/null @@ -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'), - ), - ] diff --git a/InvenTree/build/migrations/0021_auto_20201020_0908_squashed_0026_auto_20201023_1228.py b/InvenTree/build/migrations/0021_auto_20201020_0908_squashed_0026_auto_20201023_1228.py new file mode 100644 index 0000000000..8db4a7f952 --- /dev/null +++ b/InvenTree/build/migrations/0021_auto_20201020_0908_squashed_0026_auto_20201023_1228.py @@ -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')}, + ), + ] diff --git a/InvenTree/build/migrations/0022_auto_20201020_0953.py b/InvenTree/build/migrations/0022_auto_20201020_0953.py deleted file mode 100644 index 62a82ce7fd..0000000000 --- a/InvenTree/build/migrations/0022_auto_20201020_0953.py +++ /dev/null @@ -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'), - ), - ] diff --git a/InvenTree/build/migrations/0023_auto_20201020_1009.py b/InvenTree/build/migrations/0023_auto_20201020_1009.py deleted file mode 100644 index be5652d043..0000000000 --- a/InvenTree/build/migrations/0023_auto_20201020_1009.py +++ /dev/null @@ -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'), - ), - ] diff --git a/InvenTree/build/migrations/0024_auto_20201020_1144.py b/InvenTree/build/migrations/0024_auto_20201020_1144.py deleted file mode 100644 index 2d7c649cd5..0000000000 --- a/InvenTree/build/migrations/0024_auto_20201020_1144.py +++ /dev/null @@ -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'), - ), - ] diff --git a/InvenTree/build/migrations/0025_auto_20201020_1248.py b/InvenTree/build/migrations/0025_auto_20201020_1248.py deleted file mode 100644 index ecc0b73ac9..0000000000 --- a/InvenTree/build/migrations/0025_auto_20201020_1248.py +++ /dev/null @@ -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'), - ), - ] diff --git a/InvenTree/build/migrations/0026_auto_20201023_1228.py b/InvenTree/build/migrations/0026_auto_20201023_1228.py deleted file mode 100644 index cef13c534c..0000000000 --- a/InvenTree/build/migrations/0026_auto_20201023_1228.py +++ /dev/null @@ -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')}, - ), - ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index d462513f9b..68adf68a6d 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -670,6 +670,28 @@ class BuildItem(models.Model): ('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): """ Check validity of the BuildItem model. The following checks are performed: @@ -677,8 +699,10 @@ class BuildItem(models.Model): - StockItem.part must be in the BOM of the Part object referenced by Build - Allocation quantity cannot exceed available quantity """ + + self.validate_unique() - super(BuildItem, self).clean() + super().clean() errors = {} @@ -711,6 +735,14 @@ class BuildItem(models.Model): if not self.install_into.part == self.build.part: 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): pass diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index e69853c269..e3c424a363 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -3,7 +3,6 @@ from django.test import TestCase from django.core.exceptions import ValidationError -from django.db import transaction from django.db.utils import IntegrityError from build.models import Build, BuildItem @@ -144,13 +143,15 @@ class BuildTest(TestCase): quantity=q21 ) - with transaction.atomic(): - with self.assertRaises(IntegrityError): - BuildItem.objects.create( - build=self.build, - stock_item=self.stock_2_1, - quantity=99 - ) + # Attempt to create another identical BuildItem + b = BuildItem( + build=self.build, + stock_item=self.stock_2_1, + quantity=q21 + ) + + with self.assertRaises(ValidationError): + b.clean() self.assertEqual(BuildItem.objects.count(), 3)