2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +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

@ -356,6 +356,7 @@ class StockFilter(rest_filters.FilterSet):
'belongs_to',
'build',
'customer',
'consumed_by',
'sales_order',
'purchase_order',
'tags__name',

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.19 on 2023-05-14 23:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0042_alter_build_notes'),
('stock', '0100_auto_20230515_0004'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='consumed_by',
field=models.ForeignKey(blank=True, help_text='Build order which consumed this stock item', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consumed_stock', to='build.build', verbose_name='Consumed By'),
),
]

View File

@ -332,6 +332,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
sales_order=None,
belongs_to=None,
customer=None,
consumed_by=None,
is_building=False,
status__in=StockStatus.AVAILABLE_CODES
)
@ -755,6 +756,14 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
related_name='build_outputs',
)
consumed_by = models.ForeignKey(
'build.Build', on_delete=models.CASCADE,
verbose_name=_('Consumed By'),
blank=True, null=True,
help_text=_('Build order which consumed this stock item'),
related_name='consumed_stock',
)
is_building = models.BooleanField(
default=False,
)
@ -1167,7 +1176,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
return self.installed_parts.count()
@transaction.atomic
def installStockItem(self, other_item, quantity, user, notes):
def installStockItem(self, other_item, quantity, user, notes, build=None):
"""Install another stock item into this stock item.
Args:
@ -1175,6 +1184,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
quantity: The quantity of stock to install
user: The user performing the operation
notes: Any notes associated with the operation
build: The BuildOrder to associate with the operation (optional)
"""
# If the quantity is less than the stock item, split the stock!
stock_item = other_item.splitStock(quantity, None, user)
@ -1184,16 +1194,22 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
# Assign the other stock item into this one
stock_item.belongs_to = self
stock_item.save()
stock_item.consumed_by = build
stock_item.save(add_note=False)
deltas = {
'stockitem': self.pk,
}
if build is not None:
deltas['buildorder'] = build.pk
# Add a transaction note to the other item
stock_item.add_tracking_entry(
StockHistoryCode.INSTALLED_INTO_ASSEMBLY,
user,
notes=notes,
deltas={
'stockitem': self.pk,
}
deltas=deltas,
)
# Add a transaction note to this item (the assembly)
@ -1574,7 +1590,6 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
The new item will have a different StockItem ID, while this will remain the same.
"""
notes = kwargs.get('notes', '')
code = kwargs.get('code', StockHistoryCode.SPLIT_FROM_PARENT)
# Do not split a serialized part
if self.serialized:
@ -1606,30 +1621,31 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
else:
new_stock.location = self.location
new_stock.save()
new_stock.save(add_note=False)
# Copy the transaction history of this part into the new one
new_stock.copyHistoryFrom(self)
# Add a stock tracking entry for the newly created item
new_stock.add_tracking_entry(
StockHistoryCode.SPLIT_FROM_PARENT,
user,
quantity=quantity,
notes=notes,
location=location,
deltas={
'stockitem': self.pk,
}
)
# Copy the test results of this part to the new one
new_stock.copyTestResultsFrom(self)
# Add a new tracking item for the new stock item
new_stock.add_tracking_entry(
code,
user,
notes=notes,
deltas={
'stockitem': self.pk,
},
location=location,
)
# Remove the specified quantity from THIS stock item
self.take_stock(
quantity,
user,
notes=notes
code=StockHistoryCode.SPLIT_CHILD_ITEM,
notes=notes,
location=location,
stockitem=new_stock,
)
# Return a copy of the "new" stock item
@ -1798,7 +1814,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
return True
@transaction.atomic
def take_stock(self, quantity, user, notes='', code=StockHistoryCode.STOCK_REMOVE):
def take_stock(self, quantity, user, notes='', code=StockHistoryCode.STOCK_REMOVE, **kwargs):
"""Remove items from stock."""
# Cannot remove items from a serialized part
if self.serialized:
@ -1814,14 +1830,22 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
if self.updateQuantity(self.quantity - quantity):
deltas = {
'removed': float(quantity),
'quantity': float(self.quantity),
}
if location := kwargs.get('location', None):
deltas['location'] = location.pk
if stockitem := kwargs.get('stockitem', None):
deltas['stockitem'] = stockitem.pk
self.add_tracking_entry(
code,
user,
notes=notes,
deltas={
'removed': float(quantity),
'quantity': float(self.quantity),
}
deltas=deltas,
)
return True

View File

@ -93,6 +93,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
'batch',
'belongs_to',
'build',
'consumed_by',
'customer',
'delete_on_deplete',
'expired',

View File

@ -315,11 +315,9 @@
ancestor: {{ item.id }},
},
name: 'item-childs',
groupByField: 'location',
buttons: [
'#stock-options',
],
url: "{% url 'api-stock-list' %}",
});
{% endif %}

View File

@ -358,6 +358,12 @@
<a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a>
</td>
</tr>
{% elif item.consumed_by %}
<tr>
<td><span class='fas fa-tools'></span></td>
<td>{% trans "Consumed By" %}</td>
<td><a href='{% url "build-detail" item.consumed_by.pk %}'>{{ item.consumed_by }}</td>
</tr>
{% elif item.sales_order %}
<tr>
<td><span class='fas fa-th-list'></span></td>

View File

@ -423,7 +423,6 @@
location_detail: true,
supplier_part_detail: true,
},
url: "{% url 'api-stock-list' %}",
});
});