mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1020 from SchrodingersGat/stock-building
Add "is_building" field to StockItem model
This commit is contained in:
commit
cc05220263
Binary file not shown.
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
@ -765,20 +765,30 @@ class BomList(generics.ListCreateAPIView):
|
||||
|
||||
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?
|
||||
part = self.request.query_params.get('part', None)
|
||||
part = params.get('part', None)
|
||||
|
||||
if part is not None:
|
||||
queryset = queryset.filter(part=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:
|
||||
queryset = queryset.filter(sub_part=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:
|
||||
trackable = str2bool(trackable)
|
||||
|
@ -231,7 +231,8 @@ class EditBomItemForm(HelperForm):
|
||||
'quantity',
|
||||
'reference',
|
||||
'overage',
|
||||
'note'
|
||||
'note',
|
||||
'optional',
|
||||
]
|
||||
|
||||
# Prevent editing of the part associated with this BomItem
|
||||
|
18
InvenTree/part/migrations/0051_bomitem_optional.py
Normal file
18
InvenTree/part/migrations/0051_bomitem_optional.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -784,12 +784,13 @@ class Part(MPTTModel):
|
||||
""" 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:
|
||||
quantity = 0
|
||||
query = stock_items.aggregate(
|
||||
quantity=Coalesce(Sum('quantity'), Decimal(0))
|
||||
)
|
||||
|
||||
return quantity
|
||||
return query['quantity']
|
||||
|
||||
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)
|
||||
sub_part: Link to the child part (the part that will be consumed)
|
||||
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)
|
||||
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
|
||||
@ -1532,6 +1534,8 @@ class BomItem(models.Model):
|
||||
# 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'))
|
||||
|
||||
optional = models.BooleanField(default=False, help_text=_("This BOM item is optional"))
|
||||
|
||||
overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage],
|
||||
help_text=_('Estimated build wastage quantity (absolute or percentage)')
|
||||
)
|
||||
|
@ -403,6 +403,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
'quantity',
|
||||
'reference',
|
||||
'price_range',
|
||||
'optional',
|
||||
'overage',
|
||||
'note',
|
||||
'validated',
|
||||
|
18
InvenTree/stock/migrations/0052_stockitem_is_building.py
Normal file
18
InvenTree/stock/migrations/0052_stockitem_is_building.py
Normal 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),
|
||||
),
|
||||
]
|
@ -130,6 +130,7 @@ class StockItem(MPTTModel):
|
||||
status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus)
|
||||
notes: Extra notes field
|
||||
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)
|
||||
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)
|
||||
@ -142,6 +143,7 @@ class StockItem(MPTTModel):
|
||||
build_order=None,
|
||||
belongs_to=None,
|
||||
customer=None,
|
||||
is_building=False,
|
||||
status__in=StockStatus.AVAILABLE_CODES
|
||||
)
|
||||
|
||||
@ -273,11 +275,25 @@ class StockItem(MPTTModel):
|
||||
# TODO - Find a test than can be perfomed...
|
||||
pass
|
||||
|
||||
# Ensure that the item cannot be assigned to itself
|
||||
if self.belongs_to and self.belongs_to.pk == self.pk:
|
||||
raise ValidationError({
|
||||
'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):
|
||||
return reverse('stock-item-detail', kwargs={'pk': self.id})
|
||||
|
||||
@ -389,6 +405,10 @@ class StockItem(MPTTModel):
|
||||
related_name='build_outputs',
|
||||
)
|
||||
|
||||
is_building = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
purchase_order = models.ForeignKey(
|
||||
'order.PurchaseOrder',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -715,6 +735,10 @@ class StockItem(MPTTModel):
|
||||
if self.customer is not None:
|
||||
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
|
||||
if self.status in StockStatus.UNAVAILABLE_CODES:
|
||||
return False
|
||||
|
@ -7,7 +7,9 @@ import datetime
|
||||
|
||||
from .models import StockLocation, StockItem, StockItemTracking
|
||||
from .models import StockItemTestResult
|
||||
|
||||
from part.models import Part
|
||||
from build.models import Build
|
||||
|
||||
|
||||
class StockTest(TestCase):
|
||||
@ -47,6 +49,35 @@ class StockTest(TestCase):
|
||||
Part.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):
|
||||
self.assertEqual(StockLocation.objects.count(), 7)
|
||||
|
||||
|
@ -169,6 +169,10 @@ function loadBomTable(table, options) {
|
||||
// Let's make it a bit more pretty
|
||||
text = parseFloat(text);
|
||||
|
||||
if (row.optional) {
|
||||
text += " ({% trans "Optional" %})";
|
||||
}
|
||||
|
||||
if (row.overage) {
|
||||
text += "<small> (+" + row.overage + ") </small>";
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user