Adds ability to partially scrap build outputs (#4846)

* BuildOrder updates:

- Use batch code generation when creating a new build output
- Allow partial scrapping of build outputs

* Fixes for stock table

* Bump API version

* Update unit tests
This commit is contained in:
Oliver 2023-05-18 14:04:57 +10:00 committed by GitHub
parent 120a710ad4
commit 327381357b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 13 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version
INVENTREE_API_VERSION = 113
INVENTREE_API_VERSION = 115
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v115 - > 2023-05-18 : https://github.com/inventree/InvenTree/pull/4846
- Adds ability to partially scrap a build output
v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825
- Adds "delivery_date" to shipments

View File

@ -797,7 +797,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
items.all().delete()
@transaction.atomic
def scrap_build_output(self, output, location, **kwargs):
def scrap_build_output(self, output, quantity, location, **kwargs):
"""Mark a particular build output as scrapped / rejected
- Mark the output as "complete"
@ -809,10 +809,25 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
if not output:
raise ValidationError(_("No build output specified"))
if quantity <= 0:
raise ValidationError({
'quantity': _("Quantity must be greater than zero")
})
if quantity > output.quantity:
raise ValidationError({
'quantity': _("Quantity cannot be greater than the output quantity")
})
user = kwargs.get('user', None)
notes = kwargs.get('notes', '')
discard_allocations = kwargs.get('discard_allocations', False)
if quantity < output.quantity:
# Split output into two items
output = output.splitStock(quantity, location=location, user=user)
output.build = self
# Update build output item
output.is_building = False
output.status = StockStatus.REJECTED

View File

@ -17,7 +17,7 @@ import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.status_codes import StockStatus
from stock.models import StockItem, StockLocation
from stock.models import generate_batch_code, StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer
from part.models import BomItem
@ -181,6 +181,45 @@ class BuildOutputSerializer(serializers.Serializer):
return output
class BuildOutputQuantitySerializer(BuildOutputSerializer):
"""Serializer for a single build output, with additional quantity field"""
class Meta:
"""Serializer metaclass"""
fields = BuildOutputSerializer.Meta.fields + [
'quantity',
]
quantity = serializers.DecimalField(
max_digits=15,
decimal_places=5,
min_value=0,
required=True,
label=_('Quantity'),
help_text=_('Enter quantity for build output'),
)
def validate(self, data):
"""Validate the serializer data"""
data = super().validate(data)
output = data.get('output')
quantity = data.get('quantity')
if quantity <= 0:
raise ValidationError({
'quantity': _('Quantity must be greater than zero')
})
if quantity > output.quantity:
raise ValidationError({
'quantity': _("Quantity cannot be greater than the output quantity")
})
return data
class BuildOutputCreateSerializer(serializers.Serializer):
"""Serializer for creating a new BuildOutput against a BuildOrder.
@ -226,6 +265,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
batch_code = serializers.CharField(
required=False,
allow_blank=True,
default=generate_batch_code,
label=_('Batch Code'),
help_text=_('Batch code for this build output'),
)
@ -362,7 +402,7 @@ class BuildOutputScrapSerializer(serializers.Serializer):
'notes',
]
outputs = BuildOutputSerializer(
outputs = BuildOutputQuantitySerializer(
many=True,
required=True,
)
@ -412,8 +452,10 @@ class BuildOutputScrapSerializer(serializers.Serializer):
with transaction.atomic():
for item in outputs:
output = item['output']
quantity = item['quantity']
build.scrap_build_output(
output,
quantity,
data.get('location', None),
user=request.user,
notes=data.get('notes', ''),

View File

@ -302,7 +302,7 @@
</div>
<div class='panel-content'>
{% include "stock_table.html" with read_only=True prefix="build-" %}
{% include "stock_table.html" with prefix="build-" %}
</div>
</div>
@ -343,6 +343,7 @@
onPanelLoad('consumed', function() {
loadStockTable($('#consumed-stock-table'), {
filterTarget: '#filter-list-consumed-stock',
params: {
location_detail: true,
part_detail: true,
@ -354,6 +355,7 @@ onPanelLoad('consumed', function() {
onPanelLoad('completed', function() {
loadStockTable($("#build-stock-table"), {
filterTarget: '#filter-list-build-stock',
params: {
location_detail: true,
part_detail: true,

View File

@ -1027,12 +1027,15 @@ class BuildOutputScrapTest(BuildAPITest):
'outputs': [
{
'output': outputs[0].pk,
'quantity': outputs[0].quantity,
},
{
'output': outputs[1].pk,
'quantity': outputs[1].quantity,
},
{
'output': outputs[2].pk,
'quantity': outputs[2].quantity,
},
],
'location': 1,

View File

@ -1575,7 +1575,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
self.save()
@transaction.atomic
def splitStock(self, quantity, location, user, **kwargs):
def splitStock(self, quantity, location=None, user=None, **kwargs):
"""Split this stock item into two items, in the same location.
Stock tracking notes for this StockItem will be duplicated,
@ -1585,9 +1585,11 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
quantity: Number of stock items to remove from this entity, and pass to the next
location: Where to move the new StockItem to
Notes:
The provided quantity will be subtracted from this item and given to the new one.
The new item will have a different StockItem ID, while this will remain the same.
Returns:
The new StockItem object
- The provided quantity will be subtracted from this item and given to the new one.
- The new item will have a different StockItem ID, while this will remain the same.
"""
notes = kwargs.get('notes', '')

View File

@ -1 +1 @@
<div class='filter-list d-flex flex-row form-row' id='filter-list-{{ id }}'><!-- Empty div for table filters --></div>
<div class='filter-list d-flex flex-row form-row' id='filter-list-{% if prefix %}{{ prefix }}{% endif %}{{ id }}'><!-- Empty div for table filters --></div>

View File

@ -463,7 +463,7 @@ function unallocateStock(build_id, options={}) {
/*
* Helper function to render a single build output in a modal form
*/
function renderBuildOutput(output, opts={}) {
function renderBuildOutput(output, options={}) {
let pk = output.pk;
let output_html = imageHoverIcon(output.part_detail.thumbnail);
@ -494,10 +494,31 @@ function renderBuildOutput(output, opts={}) {
}
);
let quantity_field = '';
if (options.adjust_quantity) {
quantity_field = constructField(
`outputs_quantity_${pk}`,
{
type: 'decimal',
value: output.quantity,
min_value: 0,
max_value: output.quantity,
required: true,
},
{
hideLabels: true,
}
);
quantity_field = `<td>${quantity_field}</td>`;
}
let html = `
<tr id='output_row_${pk}'>
<td>${field}</td>
<td>${output.part_detail.full_name}</td>
${quantity_field}
<td>${buttons}</td>
</tr>`;
@ -645,7 +666,9 @@ function scrapBuildOutputs(build_id, outputs, options={}) {
let table_entries = '';
outputs.forEach(function(output) {
table_entries += renderBuildOutput(output);
table_entries += renderBuildOutput(output, {
adjust_quantity: true,
});
});
var html = `
@ -660,6 +683,7 @@ function scrapBuildOutputs(build_id, outputs, options={}) {
<table class='table table-striped table-condensed' id='build-scrap-table'>
<thead>
<th colspan='2'>{% trans "Output" %}</th>
<th>{% trans "Quantity" %}</th>
<th><!-- Actions --></th>
</thead>
<tbody>
@ -701,11 +725,14 @@ function scrapBuildOutputs(build_id, outputs, options={}) {
outputs.forEach(function(output) {
let pk = output.pk;
let row = $(opts.modal).find(`#output_row_${pk}`);
let quantity = getFormFieldValue(`outputs_quantity_${pk}`, {}, opts);
if (row.exists()) {
data.outputs.push({
output: pk,
quantity: quantity,
});
output_pk_values.push(pk);
}
});

View File

@ -45,7 +45,7 @@
</div>
{% endif %}
{% endif %}
{% include "filter_list.html" with id="stock" %}
{% include "filter_list.html" with prefix=prefix id="stock" %}
</div>
</div>
</div>