2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 04:25:42 +00:00

Build consume stock (#4817)

* Adds "consumed_by" field to the StockItem model.

- Points to a BuildOrder instance which "consumed" this stock
- Marks item as unavailable
- Allows filtering against build order

* Allow API filtering

* Adds table of "consumed stock items" to build order page

* Update stock table to show "consumed by" stock status

* Add "consumed_by" link to stock item detail

* Optionally add 'buildorder' details to installStockItem method

* Update methodology for completing a build item

- Instead of deleting stock, mark as "consumed by"

* Fix history entry for splitting stock

* Bug fix

* track "consumed_by" field for tracked items also

* Update build docs

* Update allocation documentation

* Update terminology.md

* Unit test updates

* Fix conflicting migrations

* revert change
This commit is contained in:
Oliver
2023-05-16 21:25:02 +10:00
committed by GitHub
parent 368f615d71
commit 397419f365
21 changed files with 207 additions and 95 deletions

View File

@ -1349,39 +1349,48 @@ class BuildItem(InvenTree.models.MetadataMixin, models.Model):
"""Complete the allocation of this BuildItem into the output stock item.
- If the referenced part is trackable, the stock item will be *installed* into the build output
- If the referenced part is *not* trackable, the stock item will be removed from stock
- If the referenced part is *not* trackable, the stock item will be *consumed* by the build order
"""
item = self.stock_item
# Split the allocated stock if there are more available than allocated
if item.quantity > self.quantity:
item = item.splitStock(
self.quantity,
None,
user,
notes=notes,
)
# For a trackable part, special consideration needed!
if item.part.trackable:
# Split the allocated stock if there are more available than allocated
if item.quantity > self.quantity:
item = item.splitStock(
self.quantity,
None,
user,
code=StockHistoryCode.BUILD_CONSUMED,
)
# Make sure we are pointing to the new item
self.stock_item = item
self.save()
# Make sure we are pointing to the new item
self.stock_item = item
self.save()
# Install the stock item into the output
self.install_into.installStockItem(
item,
self.quantity,
user,
notes
notes,
build=self.build,
)
else:
# Simply remove the items from stock
item.take_stock(
self.quantity,
# Mark the item as "consumed" by the build order
item.consumed_by = self.build
item.save(add_note=False)
item.add_tracking_entry(
StockHistoryCode.BUILD_CONSUMED,
user,
code=StockHistoryCode.BUILD_CONSUMED
notes=notes,
deltas={
'buildorder': self.build.pk,
'quantity': float(item.quantity),
}
)
def getStockItemThumbnail(self):

View File

@ -282,6 +282,18 @@
</div>
</div>
<div class='panel panel-hidden' id='panel-consumed'>
<div class='panel-heading'>
<h4>
{% trans "Consumed Stock" %}
</h4>
</div>
<div class='panel-content'>
{% include "stock_table.html" with read_only=True prefix="consumed-" %}
</div>
</div>
<div class='panel panel-hidden' id='panel-completed'>
<div class='panel-heading'>
<h4>
@ -329,6 +341,17 @@
{% block js_ready %}
{{ block.super }}
onPanelLoad('consumed', function() {
loadStockTable($('#consumed-stock-table'), {
params: {
location_detail: true,
part_detail: true,
consumed_by: {{ build.pk }},
in_stock: false,
},
});
});
onPanelLoad('completed', function() {
loadStockTable($("#build-stock-table"), {
params: {
@ -337,11 +360,9 @@ onPanelLoad('completed', function() {
build: {{ build.id }},
is_building: false,
},
groupByField: 'location',
buttons: [
'#stock-options',
],
url: "{% url 'api-stock-list' %}",
});
});

View File

@ -8,7 +8,9 @@
{% trans "Allocate Stock" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
{% endif %}
{% if not build.is_complete %}
{% trans "Consumed Stock" as text %}
{% include "sidebar_item.html" with label='consumed' text=text icon="fa-list" %}
{% if build.is_active %}
{% trans "Incomplete Outputs" as text %}
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
{% endif %}

View File

@ -424,6 +424,7 @@ class BuildTest(BuildTestBase):
extra_2_2: 4, # 35
}
)
self.assertTrue(self.build.has_overallocated_parts(None))
self.build.trim_allocated_stock()
@ -433,15 +434,30 @@ class BuildTest(BuildTestBase):
self.build.complete_build_output(self.output_2, None)
self.assertTrue(self.build.can_complete)
n = StockItem.objects.filter(consumed_by=self.build).count()
self.build.complete_build(None)
self.assertEqual(self.build.status, status.BuildStatus.COMPLETE)
# Check stock items are in expected state.
self.assertEqual(StockItem.objects.get(pk=self.stock_1_2.pk).quantity, 53)
self.assertEqual(StockItem.objects.filter(part=self.sub_part_2).aggregate(Sum('quantity'))['quantity__sum'], 5)
# Total stock quantity has not been decreased
items = StockItem.objects.filter(part=self.sub_part_2)
self.assertEqual(items.aggregate(Sum('quantity'))['quantity__sum'], 35)
# However, the "available" stock quantity has been decreased
self.assertEqual(items.filter(consumed_by=None).aggregate(Sum('quantity'))['quantity__sum'], 5)
# And the "consumed_by" quantity has been increased
self.assertEqual(items.filter(consumed_by=self.build).aggregate(Sum('quantity'))['quantity__sum'], 30)
self.assertEqual(StockItem.objects.get(pk=self.stock_3_1.pk).quantity, 980)
# Check that the "consumed_by" item count has increased
self.assertEqual(StockItem.objects.filter(consumed_by=self.build).count(), n + 8)
def test_cancel(self):
"""Test cancellation of the build"""
@ -510,15 +526,12 @@ class BuildTest(BuildTestBase):
self.assertEqual(BuildItem.objects.count(), 0)
# New stock items should have been created!
self.assertEqual(StockItem.objects.count(), 10)
self.assertEqual(StockItem.objects.count(), 13)
# This stock item has been depleted!
with self.assertRaises(StockItem.DoesNotExist):
StockItem.objects.get(pk=self.stock_1_1.pk)
# This stock item has also been depleted
with self.assertRaises(StockItem.DoesNotExist):
StockItem.objects.get(pk=self.stock_2_1.pk)
# This stock item has been marked as "consumed"
item = StockItem.objects.get(pk=self.stock_1_1.pk)
self.assertIsNotNone(item.consumed_by)
self.assertFalse(item.in_stock)
# And 10 new stock items created for the build output
outputs = StockItem.objects.filter(build=self.build)