Merge pull request #1020 from SchrodingersGat/stock-building

Add "is_building" field to StockItem model
This commit is contained in:
Oliver 2020-10-05 10:08:40 +11:00 committed by GitHub
commit cc05220263
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1867 additions and 1431 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -765,20 +765,30 @@ class BomList(generics.ListCreateAPIView):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by "optional" status?
optional = params.get('optional', None)
if optional is not None:
optional = str2bool(optional)
queryset = queryset.filter(optional=optional)
# Filter by part? # Filter by part?
part = self.request.query_params.get('part', None) part = params.get('part', None)
if part is not None: if part is not None:
queryset = queryset.filter(part=part) queryset = queryset.filter(part=part)
# Filter by sub-part? # Filter by sub-part?
sub_part = self.request.query_params.get('sub_part', None) sub_part = params.get('sub_part', None)
if sub_part is not None: if sub_part is not None:
queryset = queryset.filter(sub_part=sub_part) queryset = queryset.filter(sub_part=sub_part)
# Filter by "trackable" status of the sub-part # Filter by "trackable" status of the sub-part
trackable = self.request.query_params.get('trackable', None) trackable = params.get('trackable', None)
if trackable is not None: if trackable is not None:
trackable = str2bool(trackable) trackable = str2bool(trackable)

View File

@ -231,7 +231,8 @@ class EditBomItemForm(HelperForm):
'quantity', 'quantity',
'reference', 'reference',
'overage', 'overage',
'note' 'note',
'optional',
] ]
# Prevent editing of the part associated with this BomItem # Prevent editing of the part associated with this BomItem

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-10-04 13:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0050_auto_20200917_2315'),
]
operations = [
migrations.AddField(
model_name='bomitem',
name='optional',
field=models.BooleanField(default=False, help_text='This BOM item is optional'),
),
]

View File

@ -784,12 +784,13 @@ class Part(MPTTModel):
""" Return the current number of parts currently being built """ Return the current number of parts currently being built
""" """
quantity = self.active_builds.aggregate(quantity=Sum('quantity'))['quantity'] stock_items = self.stock_items.filter(is_building=True)
if quantity is None: query = stock_items.aggregate(
quantity = 0 quantity=Coalesce(Sum('quantity'), Decimal(0))
)
return quantity return query['quantity']
def build_order_allocations(self): def build_order_allocations(self):
""" """
@ -1499,6 +1500,7 @@ class BomItem(models.Model):
part: Link to the parent part (the part that will be produced) part: Link to the parent part (the part that will be produced)
sub_part: Link to the child part (the part that will be consumed) sub_part: Link to the child part (the part that will be consumed)
quantity: Number of 'sub_parts' consumed to produce one 'part' quantity: Number of 'sub_parts' consumed to produce one 'part'
optional: Boolean field describing if this BomItem is optional
reference: BOM reference field (e.g. part designators) reference: BOM reference field (e.g. part designators)
overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%') overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%')
note: Note field for this BOM item note: Note field for this BOM item
@ -1532,6 +1534,8 @@ class BomItem(models.Model):
# Quantity required # Quantity required
quantity = models.DecimalField(default=1.0, max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], help_text=_('BOM quantity for this BOM item')) quantity = models.DecimalField(default=1.0, max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], help_text=_('BOM quantity for this BOM item'))
optional = models.BooleanField(default=False, help_text=_("This BOM item is optional"))
overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage], overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage],
help_text=_('Estimated build wastage quantity (absolute or percentage)') help_text=_('Estimated build wastage quantity (absolute or percentage)')
) )

View File

@ -403,6 +403,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
'quantity', 'quantity',
'reference', 'reference',
'price_range', 'price_range',
'optional',
'overage', 'overage',
'note', 'note',
'validated', 'validated',

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-10-04 13:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0051_auto_20200928_0928'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='is_building',
field=models.BooleanField(default=False),
),
]

View File

@ -130,6 +130,7 @@ class StockItem(MPTTModel):
status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus)
notes: Extra notes field notes: Extra notes field
build: Link to a Build (if this stock item was created from a build) build: Link to a Build (if this stock item was created from a build)
is_building: Boolean field indicating if this stock item is currently being built
purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder)
infinite: If True this StockItem can never be exhausted infinite: If True this StockItem can never be exhausted
sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder)
@ -142,6 +143,7 @@ class StockItem(MPTTModel):
build_order=None, build_order=None,
belongs_to=None, belongs_to=None,
customer=None, customer=None,
is_building=False,
status__in=StockStatus.AVAILABLE_CODES status__in=StockStatus.AVAILABLE_CODES
) )
@ -273,11 +275,25 @@ class StockItem(MPTTModel):
# TODO - Find a test than can be perfomed... # TODO - Find a test than can be perfomed...
pass pass
# Ensure that the item cannot be assigned to itself
if self.belongs_to and self.belongs_to.pk == self.pk: if self.belongs_to and self.belongs_to.pk == self.pk:
raise ValidationError({ raise ValidationError({
'belongs_to': _('Item cannot belong to itself') 'belongs_to': _('Item cannot belong to itself')
}) })
# If the item is marked as "is_building", it must point to a build!
if self.is_building and not self.build:
raise ValidationError({
'build': _("Item must have a build reference if is_building=True")
})
# If the item points to a build, check that the Part references match
if self.build:
if not self.part == self.build.part:
raise ValidationError({
'build': _("Build reference does not point to the same part object")
})
def get_absolute_url(self): def get_absolute_url(self):
return reverse('stock-item-detail', kwargs={'pk': self.id}) return reverse('stock-item-detail', kwargs={'pk': self.id})
@ -389,6 +405,10 @@ class StockItem(MPTTModel):
related_name='build_outputs', related_name='build_outputs',
) )
is_building = models.BooleanField(
default=False,
)
purchase_order = models.ForeignKey( purchase_order = models.ForeignKey(
'order.PurchaseOrder', 'order.PurchaseOrder',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -715,6 +735,10 @@ class StockItem(MPTTModel):
if self.customer is not None: if self.customer is not None:
return False return False
# Not 'in stock' if it is building
if self.is_building:
return False
# Not 'in stock' if the status code makes it unavailable # Not 'in stock' if the status code makes it unavailable
if self.status in StockStatus.UNAVAILABLE_CODES: if self.status in StockStatus.UNAVAILABLE_CODES:
return False return False

View File

@ -7,7 +7,9 @@ import datetime
from .models import StockLocation, StockItem, StockItemTracking from .models import StockLocation, StockItem, StockItemTracking
from .models import StockItemTestResult from .models import StockItemTestResult
from part.models import Part from part.models import Part
from build.models import Build
class StockTest(TestCase): class StockTest(TestCase):
@ -47,6 +49,35 @@ class StockTest(TestCase):
Part.objects.rebuild() Part.objects.rebuild()
StockItem.objects.rebuild() StockItem.objects.rebuild()
def test_is_building(self):
"""
Test that the is_building flag does not count towards stock.
"""
part = Part.objects.get(pk=1)
# Record the total stock count
n = part.total_stock
StockItem.objects.create(part=part, quantity=5)
# And there should be *no* items being build
self.assertEqual(part.quantity_being_built, 0)
build = Build.objects.create(part=part, title='A test build', quantity=1)
# Add some stock items which are "building"
for i in range(10):
StockItem.objects.create(
part=part, build=build,
quantity=10, is_building=True
)
# The "is_building" quantity should not be counted here
self.assertEqual(part.total_stock, n + 5)
self.assertEqual(part.quantity_being_built, 100)
def test_loc_count(self): def test_loc_count(self):
self.assertEqual(StockLocation.objects.count(), 7) self.assertEqual(StockLocation.objects.count(), 7)

View File

@ -169,6 +169,10 @@ function loadBomTable(table, options) {
// Let's make it a bit more pretty // Let's make it a bit more pretty
text = parseFloat(text); text = parseFloat(text);
if (row.optional) {
text += " ({% trans "Optional" %})";
}
if (row.overage) { if (row.overage) {
text += "<small> (+" + row.overage + ") </small>"; text += "<small> (+" + row.overage + ") </small>";
} }