mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
120a710ad4
commit
327381357b
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# 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
|
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
|
v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825
|
||||||
- Adds "delivery_date" to shipments
|
- Adds "delivery_date" to shipments
|
||||||
|
|
||||||
|
@ -797,7 +797,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
items.all().delete()
|
items.all().delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@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 a particular build output as scrapped / rejected
|
||||||
|
|
||||||
- Mark the output as "complete"
|
- Mark the output as "complete"
|
||||||
@ -809,10 +809,25 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
if not output:
|
if not output:
|
||||||
raise ValidationError(_("No build output specified"))
|
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)
|
user = kwargs.get('user', None)
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
discard_allocations = kwargs.get('discard_allocations', False)
|
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
|
# Update build output item
|
||||||
output.is_building = False
|
output.is_building = False
|
||||||
output.status = StockStatus.REJECTED
|
output.status = StockStatus.REJECTED
|
||||||
|
@ -17,7 +17,7 @@ import InvenTree.helpers
|
|||||||
from InvenTree.serializers import InvenTreeDecimalField
|
from InvenTree.serializers import InvenTreeDecimalField
|
||||||
from InvenTree.status_codes import StockStatus
|
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 stock.serializers import StockItemSerializerBrief, LocationSerializer
|
||||||
|
|
||||||
from part.models import BomItem
|
from part.models import BomItem
|
||||||
@ -181,6 +181,45 @@ class BuildOutputSerializer(serializers.Serializer):
|
|||||||
return output
|
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):
|
class BuildOutputCreateSerializer(serializers.Serializer):
|
||||||
"""Serializer for creating a new BuildOutput against a BuildOrder.
|
"""Serializer for creating a new BuildOutput against a BuildOrder.
|
||||||
|
|
||||||
@ -226,6 +265,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
batch_code = serializers.CharField(
|
batch_code = serializers.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
|
default=generate_batch_code,
|
||||||
label=_('Batch Code'),
|
label=_('Batch Code'),
|
||||||
help_text=_('Batch code for this build output'),
|
help_text=_('Batch code for this build output'),
|
||||||
)
|
)
|
||||||
@ -362,7 +402,7 @@ class BuildOutputScrapSerializer(serializers.Serializer):
|
|||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = BuildOutputSerializer(
|
outputs = BuildOutputQuantitySerializer(
|
||||||
many=True,
|
many=True,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
@ -412,8 +452,10 @@ class BuildOutputScrapSerializer(serializers.Serializer):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for item in outputs:
|
for item in outputs:
|
||||||
output = item['output']
|
output = item['output']
|
||||||
|
quantity = item['quantity']
|
||||||
build.scrap_build_output(
|
build.scrap_build_output(
|
||||||
output,
|
output,
|
||||||
|
quantity,
|
||||||
data.get('location', None),
|
data.get('location', None),
|
||||||
user=request.user,
|
user=request.user,
|
||||||
notes=data.get('notes', ''),
|
notes=data.get('notes', ''),
|
||||||
|
@ -302,7 +302,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% include "stock_table.html" with read_only=True prefix="build-" %}
|
{% include "stock_table.html" with prefix="build-" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -343,6 +343,7 @@
|
|||||||
|
|
||||||
onPanelLoad('consumed', function() {
|
onPanelLoad('consumed', function() {
|
||||||
loadStockTable($('#consumed-stock-table'), {
|
loadStockTable($('#consumed-stock-table'), {
|
||||||
|
filterTarget: '#filter-list-consumed-stock',
|
||||||
params: {
|
params: {
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
@ -354,6 +355,7 @@ onPanelLoad('consumed', function() {
|
|||||||
|
|
||||||
onPanelLoad('completed', function() {
|
onPanelLoad('completed', function() {
|
||||||
loadStockTable($("#build-stock-table"), {
|
loadStockTable($("#build-stock-table"), {
|
||||||
|
filterTarget: '#filter-list-build-stock',
|
||||||
params: {
|
params: {
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
|
@ -1027,12 +1027,15 @@ class BuildOutputScrapTest(BuildAPITest):
|
|||||||
'outputs': [
|
'outputs': [
|
||||||
{
|
{
|
||||||
'output': outputs[0].pk,
|
'output': outputs[0].pk,
|
||||||
|
'quantity': outputs[0].quantity,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'output': outputs[1].pk,
|
'output': outputs[1].pk,
|
||||||
|
'quantity': outputs[1].quantity,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'output': outputs[2].pk,
|
'output': outputs[2].pk,
|
||||||
|
'quantity': outputs[2].quantity,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'location': 1,
|
'location': 1,
|
||||||
|
@ -1575,7 +1575,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@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.
|
"""Split this stock item into two items, in the same location.
|
||||||
|
|
||||||
Stock tracking notes for this StockItem will be duplicated,
|
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
|
quantity: Number of stock items to remove from this entity, and pass to the next
|
||||||
location: Where to move the new StockItem to
|
location: Where to move the new StockItem to
|
||||||
|
|
||||||
Notes:
|
Returns:
|
||||||
The provided quantity will be subtracted from this item and given to the new one.
|
The new StockItem object
|
||||||
The new item will have a different StockItem ID, while this will remain the same.
|
|
||||||
|
- 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', '')
|
notes = kwargs.get('notes', '')
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -463,7 +463,7 @@ function unallocateStock(build_id, options={}) {
|
|||||||
/*
|
/*
|
||||||
* Helper function to render a single build output in a modal form
|
* 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 pk = output.pk;
|
||||||
|
|
||||||
let output_html = imageHoverIcon(output.part_detail.thumbnail);
|
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 = `
|
let html = `
|
||||||
<tr id='output_row_${pk}'>
|
<tr id='output_row_${pk}'>
|
||||||
<td>${field}</td>
|
<td>${field}</td>
|
||||||
<td>${output.part_detail.full_name}</td>
|
<td>${output.part_detail.full_name}</td>
|
||||||
|
${quantity_field}
|
||||||
<td>${buttons}</td>
|
<td>${buttons}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
@ -645,7 +666,9 @@ function scrapBuildOutputs(build_id, outputs, options={}) {
|
|||||||
let table_entries = '';
|
let table_entries = '';
|
||||||
|
|
||||||
outputs.forEach(function(output) {
|
outputs.forEach(function(output) {
|
||||||
table_entries += renderBuildOutput(output);
|
table_entries += renderBuildOutput(output, {
|
||||||
|
adjust_quantity: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var html = `
|
var html = `
|
||||||
@ -660,6 +683,7 @@ function scrapBuildOutputs(build_id, outputs, options={}) {
|
|||||||
<table class='table table-striped table-condensed' id='build-scrap-table'>
|
<table class='table table-striped table-condensed' id='build-scrap-table'>
|
||||||
<thead>
|
<thead>
|
||||||
<th colspan='2'>{% trans "Output" %}</th>
|
<th colspan='2'>{% trans "Output" %}</th>
|
||||||
|
<th>{% trans "Quantity" %}</th>
|
||||||
<th><!-- Actions --></th>
|
<th><!-- Actions --></th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -701,11 +725,14 @@ function scrapBuildOutputs(build_id, outputs, options={}) {
|
|||||||
outputs.forEach(function(output) {
|
outputs.forEach(function(output) {
|
||||||
let pk = output.pk;
|
let pk = output.pk;
|
||||||
let row = $(opts.modal).find(`#output_row_${pk}`);
|
let row = $(opts.modal).find(`#output_row_${pk}`);
|
||||||
|
let quantity = getFormFieldValue(`outputs_quantity_${pk}`, {}, opts);
|
||||||
|
|
||||||
if (row.exists()) {
|
if (row.exists()) {
|
||||||
data.outputs.push({
|
data.outputs.push({
|
||||||
output: pk,
|
output: pk,
|
||||||
|
quantity: quantity,
|
||||||
});
|
});
|
||||||
|
|
||||||
output_pk_values.push(pk);
|
output_pk_values.push(pk);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "filter_list.html" with id="stock" %}
|
{% include "filter_list.html" with prefix=prefix id="stock" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user