mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16: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:
parent
6aaf178f0b
commit
ffe15763a7
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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')},
|
||||||
|
),
|
||||||
|
]
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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')},
|
|
||||||
),
|
|
||||||
]
|
|
@ -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:
|
||||||
@ -677,8 +699,10 @@ class BuildItem(models.Model):
|
|||||||
- StockItem.part must be in the BOM of the Part object referenced by Build
|
- StockItem.part must be in the BOM of the Part object referenced by Build
|
||||||
- Allocation quantity cannot exceed available quantity
|
- Allocation quantity cannot exceed available quantity
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.validate_unique()
|
||||||
|
|
||||||
super(BuildItem, self).clean()
|
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
|
||||||
|
|
||||||
|
@ -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,13 +143,15 @@ 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=q21
|
||||||
quantity=99
|
)
|
||||||
)
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
b.clean()
|
||||||
|
|
||||||
self.assertEqual(BuildItem.objects.count(), 3)
|
self.assertEqual(BuildItem.objects.count(), 3)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user