mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add forms / views for creating a new build output, and completing the build
- Also some refactoring of how forms are handled and saved
This commit is contained in:
parent
b02c87ea50
commit
500da8099b
@ -337,7 +337,7 @@ def DownloadFile(data, filename, content_type='application/text'):
|
||||
return response
|
||||
|
||||
|
||||
def ExtractSerialNumbers(serials, expected_quantity):
|
||||
def extract_serial_numbers(serials, expected_quantity):
|
||||
""" Attempt to extract serial numbers from an input string.
|
||||
- Serial numbers must be integer values
|
||||
- Serial numbers must be positive
|
||||
|
@ -210,7 +210,7 @@ class TestSerialNumberExtraction(TestCase):
|
||||
|
||||
def test_simple(self):
|
||||
|
||||
e = helpers.ExtractSerialNumbers
|
||||
e = helpers.extract_serial_numbers
|
||||
|
||||
sn = e("1-5", 5)
|
||||
self.assertEqual(len(sn), 5)
|
||||
@ -226,7 +226,7 @@ class TestSerialNumberExtraction(TestCase):
|
||||
|
||||
def test_failures(self):
|
||||
|
||||
e = helpers.ExtractSerialNumbers
|
||||
e = helpers.extract_serial_numbers
|
||||
|
||||
# Test duplicates
|
||||
with self.assertRaises(ValidationError):
|
||||
|
@ -213,26 +213,6 @@ class AjaxMixin(InvenTreeRoleMixin):
|
||||
"""
|
||||
return {}
|
||||
|
||||
def pre_save(self, obj, form, **kwargs):
|
||||
"""
|
||||
Hook for doing something *before* an object is saved.
|
||||
|
||||
obj: The object to be saved
|
||||
form: The cleaned form
|
||||
"""
|
||||
|
||||
# Do nothing by default
|
||||
pass
|
||||
|
||||
def post_save(self, obj, form, **kwargs):
|
||||
"""
|
||||
Hook for doing something *after* an object is saved.
|
||||
|
||||
"""
|
||||
|
||||
# Do nothing by default
|
||||
pass
|
||||
|
||||
def validate(self, obj, form, **kwargs):
|
||||
"""
|
||||
Hook for performing custom form validation steps.
|
||||
@ -362,7 +342,7 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
||||
form = self.get_form()
|
||||
return self.renderJsonResponse(request, form)
|
||||
|
||||
def do_save(self, form):
|
||||
def save(self, form):
|
||||
"""
|
||||
Method for actually saving the form to the database.
|
||||
Default implementation is very simple,
|
||||
@ -402,14 +382,10 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
||||
|
||||
if valid:
|
||||
|
||||
# Perform (optional) pre-save step
|
||||
self.pre_save(None, self.form)
|
||||
|
||||
# Save the object to the database
|
||||
self.do_save(self.form)
|
||||
self.save(self.form)
|
||||
|
||||
# Perform (optional) post-save step
|
||||
self.post_save(self.object, self.form)
|
||||
self.object = self.get_object()
|
||||
|
||||
# Return the PK of the newly-created object
|
||||
data['pk'] = self.object.pk
|
||||
@ -440,11 +416,14 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||
|
||||
return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data())
|
||||
|
||||
def do_save(self, form):
|
||||
def save(self, object, form, **kwargs):
|
||||
"""
|
||||
Method for updating the object in the database.
|
||||
Default implementation is very simple,
|
||||
but can be overridden if required.
|
||||
Default implementation is very simple, but can be overridden if required.
|
||||
|
||||
Args:
|
||||
object - The current object, to be updated
|
||||
form - The validated form
|
||||
"""
|
||||
|
||||
self.object = form.save()
|
||||
@ -485,22 +464,16 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||
|
||||
if valid:
|
||||
|
||||
# Perform (optional) pre-save step
|
||||
self.pre_save(self.object, form)
|
||||
|
||||
# Save the updated objec to the database
|
||||
obj = self.do_save(form)
|
||||
self.save(self.object, form)
|
||||
|
||||
# Perform (optional) post-save step
|
||||
self.post_save(obj, form)
|
||||
self.object = self.get_object()
|
||||
|
||||
# Include context data about the updated object
|
||||
data['pk'] = obj.pk
|
||||
|
||||
self.post_save(obj, form)
|
||||
data['pk'] = self.object.pk
|
||||
|
||||
try:
|
||||
data['url'] = obj.get_absolute_url()
|
||||
data['url'] = self.object.get_absolute_url()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
@ -32,12 +32,6 @@ class EditBuildForm(HelperForm):
|
||||
'reference': _('Build Order reference')
|
||||
}
|
||||
|
||||
serial_numbers = forms.CharField(
|
||||
label=_('Serial Numbers'),
|
||||
help_text=_('Serial numbers for build outputs'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
@ -46,7 +40,6 @@ class EditBuildForm(HelperForm):
|
||||
'part',
|
||||
'quantity',
|
||||
'batch',
|
||||
'serial_numbers',
|
||||
'take_from',
|
||||
'destination',
|
||||
'parent',
|
||||
@ -55,6 +48,42 @@ class EditBuildForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class BuildOutputCreateForm(HelperForm):
|
||||
"""
|
||||
Form for creating a new build output.
|
||||
"""
|
||||
|
||||
field_prefix = {
|
||||
'serial_numbers': 'fa-hashtag',
|
||||
}
|
||||
|
||||
quantity = forms.IntegerField(
|
||||
label=_('Quantity'),
|
||||
help_text=_('Enter quantity for build output'),
|
||||
)
|
||||
|
||||
serial_numbers = forms.CharField(
|
||||
label=_('Serial numbers'),
|
||||
required=False,
|
||||
help_text=_('Enter serial numbers for build outputs'),
|
||||
)
|
||||
|
||||
confirm = forms.BooleanField(
|
||||
required=True,
|
||||
label=_('Confirm'),
|
||||
help_text=_('Confirm creation of build outut'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
'quantity',
|
||||
'batch',
|
||||
'serial_numbers',
|
||||
'confirm',
|
||||
]
|
||||
|
||||
|
||||
class BuildOutputDeleteForm(HelperForm):
|
||||
"""
|
||||
Form for deleting a build output.
|
||||
@ -123,7 +152,27 @@ class AutoAllocateForm(HelperForm):
|
||||
|
||||
|
||||
class CompleteBuildForm(HelperForm):
|
||||
""" Form for marking a Build as complete """
|
||||
"""
|
||||
Form for marking a build as complete
|
||||
"""
|
||||
|
||||
confirm = forms.BooleanField(
|
||||
required=True,
|
||||
label=_('Confirm'),
|
||||
help_text=_('Mark build as complete'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
'confirm',
|
||||
]
|
||||
|
||||
|
||||
class CompleteBuildOutputForm(HelperForm):
|
||||
"""
|
||||
Form for completing a single build output
|
||||
"""
|
||||
|
||||
field_prefix = {
|
||||
'serial_numbers': 'fa-hashtag',
|
||||
|
@ -250,7 +250,7 @@ class Build(MPTTModel):
|
||||
@property
|
||||
def incomplete_outputs(self):
|
||||
"""
|
||||
Return all the "incomplete" build outputs"
|
||||
Return all the "incomplete" build outputs
|
||||
"""
|
||||
|
||||
outputs = self.get_build_outputs(complete=False)
|
||||
@ -259,6 +259,19 @@ class Build(MPTTModel):
|
||||
|
||||
return outputs
|
||||
|
||||
@property
|
||||
def incomplete_count(self):
|
||||
"""
|
||||
Return the total number of "incomplete" outputs
|
||||
"""
|
||||
|
||||
quantity = 0
|
||||
|
||||
for output in self.incomplete_outputs:
|
||||
quantity += output.quantity
|
||||
|
||||
return quantity
|
||||
|
||||
@classmethod
|
||||
def getNextBuildNumber(cls):
|
||||
"""
|
||||
@ -291,6 +304,37 @@ class Build(MPTTModel):
|
||||
|
||||
return new_ref
|
||||
|
||||
@property
|
||||
def can_complete(self):
|
||||
"""
|
||||
Returns True if this build can be "completed"
|
||||
|
||||
- Must not have any outstanding build outputs
|
||||
- 'completed' value must meet (or exceed) the 'quantity' value
|
||||
"""
|
||||
|
||||
if self.incomplete_count > 0:
|
||||
return False
|
||||
|
||||
if self.completed < self.quantity:
|
||||
return False
|
||||
|
||||
# No issues!
|
||||
return True
|
||||
|
||||
def completeBuild(self, user):
|
||||
"""
|
||||
Mark this build as complete
|
||||
"""
|
||||
|
||||
if not self.can_complete:
|
||||
return
|
||||
|
||||
self.completion_date = datetime.now().date()
|
||||
self.completed_by = user
|
||||
self.status = BuildStatus.COMPLETE
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def cancelBuild(self, user):
|
||||
""" Mark the Build as CANCELLED
|
||||
@ -408,6 +452,77 @@ class Build(MPTTModel):
|
||||
# Remove all the allocations
|
||||
allocations.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def create_build_output(self, quantity, **kwargs):
|
||||
"""
|
||||
Create a new build output against this BuildOrder.
|
||||
|
||||
args:
|
||||
quantity: The quantity of the item to produce
|
||||
|
||||
kwargs:
|
||||
batch: Override batch code
|
||||
serials: Serial numbers
|
||||
location: Override location
|
||||
"""
|
||||
|
||||
batch = kwargs.get('batch', self.batch)
|
||||
location = kwargs.get('location', self.destination)
|
||||
serials = kwargs.get('serials', None)
|
||||
|
||||
"""
|
||||
Determine if we can create a single output (with quantity > 0),
|
||||
or multiple outputs (with quantity = 1)
|
||||
"""
|
||||
|
||||
multiple = False
|
||||
|
||||
# Serial numbers are provided? We need to split!
|
||||
if serials:
|
||||
multiple = True
|
||||
|
||||
# BOM has trackable parts, so we must split!
|
||||
if self.part.has_trackable_parts:
|
||||
multiple = True
|
||||
|
||||
if multiple:
|
||||
"""
|
||||
Create multiple build outputs with a single quantity of 1
|
||||
"""
|
||||
|
||||
for ii in range(quantity):
|
||||
|
||||
if serials:
|
||||
serial = serials[ii]
|
||||
else:
|
||||
serial = None
|
||||
|
||||
output = StockModels.StockItem.objects.create(
|
||||
quantity=1,
|
||||
location=location,
|
||||
part=self.part,
|
||||
build=self,
|
||||
batch=batch,
|
||||
serial=serial,
|
||||
is_building=True,
|
||||
)
|
||||
|
||||
else:
|
||||
"""
|
||||
Create a single build output of the given quantity
|
||||
"""
|
||||
|
||||
output = StockModels.StockItem.objects.create(
|
||||
quantity=quantity,
|
||||
location=location,
|
||||
part=self.part,
|
||||
build=self,
|
||||
batch=batch,
|
||||
is_building=True
|
||||
)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def deleteBuildOutput(self, output):
|
||||
"""
|
||||
Remove a build output from the database:
|
||||
|
@ -11,11 +11,19 @@ InvenTree | Allocate Parts
|
||||
|
||||
{% include "build/tabs.html" with tab='allocate' %}
|
||||
|
||||
<div class='btn-group' role='group'>
|
||||
<h4>{% trans "Incomplete Build Ouputs" %}</h4>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class='btn-group' role='group'>
|
||||
<button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
|
||||
</button>
|
||||
<button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h4>{% trans "Incomplete Build Ouputs" %}</h4>
|
||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||
{% for item in build.incomplete_outputs %}
|
||||
{% include "build/allocation_card.html" with item=item %}
|
||||
@ -57,388 +65,6 @@ InvenTree | Allocate Parts
|
||||
);
|
||||
{% endfor %}
|
||||
|
||||
var buildTable = $("#build-item-list");
|
||||
|
||||
// Calculate sum of allocations for a particular table row
|
||||
function sumAllocations(row) {
|
||||
if (row.allocations == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var quantity = 0;
|
||||
|
||||
row.allocations.forEach(function(item) {
|
||||
quantity += item.quantity;
|
||||
});
|
||||
|
||||
return quantity;
|
||||
}
|
||||
|
||||
function getUnallocated(row) {
|
||||
// Return the number of items remaining to be allocated for a given row
|
||||
return {{ build.quantity }} * row.quantity - sumAllocations(row);
|
||||
}
|
||||
|
||||
function setExpandedAllocatedLocation(row) {
|
||||
// Handle case when stock item does not have a location set
|
||||
if (row.location_detail == null) {
|
||||
return 'No stock location set';
|
||||
} else {
|
||||
return row.location_detail.pathstring;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
// Reload the build allocation table
|
||||
buildTable.bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
function setupCallbacks() {
|
||||
// Register button callbacks once the table data are loaded
|
||||
|
||||
buildTable.find(".button-add").click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// Extract row data from the table
|
||||
var idx = $(this).closest('tr').attr('data-index');
|
||||
var row = buildTable.bootstrapTable('getData')[idx];
|
||||
|
||||
launchModalForm('/build/item/new/', {
|
||||
success: reloadTable,
|
||||
data: {
|
||||
part: row.sub_part,
|
||||
build: {{ build.id }},
|
||||
quantity: getUnallocated(row),
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'stock_item',
|
||||
label: '{% trans "New Stock Item" %}',
|
||||
title: '{% trans "Create new Stock Item" %}',
|
||||
url: '{% url "stock-item-create" %}',
|
||||
data: {
|
||||
part: row.sub_part,
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
buildTable.find(".button-build").click(function() {
|
||||
// Start a new build for the sub_part
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// Extract row data from the table
|
||||
var idx = $(this).closest('tr').attr('data-index');
|
||||
var row = buildTable.bootstrapTable('getData')[idx];
|
||||
|
||||
launchModalForm('/build/new/', {
|
||||
follow: true,
|
||||
data: {
|
||||
part: row.sub_part,
|
||||
parent: {{ build.id }},
|
||||
quantity: getUnallocated(row),
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
buildTable.find(".button-buy").click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// Extract row data from the table
|
||||
var idx = $(this).closest('tr').attr('data-index');
|
||||
var row = buildTable.bootstrapTable('getData')[idx];
|
||||
|
||||
launchModalForm("{% url 'order-parts' %}", {
|
||||
data: {
|
||||
parts: [row.sub_part],
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
buildTable.inventreeTable({
|
||||
uniqueId: 'sub_part',
|
||||
url: "{% url 'api-bom-list' %}",
|
||||
onPostBody: setupCallbacks,
|
||||
detailViewByClick: true,
|
||||
detailView: true,
|
||||
detailFilter: function(index, row) {
|
||||
return row.allocations != null;
|
||||
},
|
||||
detailFormatter: function(index, row, element) {
|
||||
// Construct an 'inner table' which shows the stock allocations
|
||||
|
||||
var subTableId = `allocation-table-${row.pk}`;
|
||||
|
||||
var html = `<div class='sub-table'><table class='table table-condensed table-striped' id='${subTableId}'></table></div>`;
|
||||
|
||||
element.html(html);
|
||||
|
||||
var lineItem = row;
|
||||
|
||||
var subTable = $(`#${subTableId}`);
|
||||
|
||||
subTable.bootstrapTable({
|
||||
data: row.allocations,
|
||||
showHeader: true,
|
||||
columns: [
|
||||
{
|
||||
width: '50%',
|
||||
field: 'quantity',
|
||||
title: 'Quantity',
|
||||
formatter: function(value, row, index, field) {
|
||||
var text = '';
|
||||
|
||||
var url = '';
|
||||
|
||||
if (row.serial && row.quantity == 1) {
|
||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||
} else {
|
||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||
}
|
||||
|
||||
{% if build.status == BuildStatus.COMPLETE %}
|
||||
url = `/stock/item/${row.pk}/`;
|
||||
{% else %}
|
||||
url = `/stock/item/${row.stock_item}/`;
|
||||
{% endif %}
|
||||
|
||||
return renderLink(text, url);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: '{% trans "Location" %}',
|
||||
formatter: function(value, row, index, field) {
|
||||
{% if build.status == BuildStatus.COMPLETE %}
|
||||
var text = setExpandedAllocatedLocation(row);
|
||||
var url = `/stock/location/${row.location}/`;
|
||||
{% else %}
|
||||
var text = row.stock_item_detail.location_name;
|
||||
var url = `/stock/location/${row.stock_item_detail.location}/`;
|
||||
{% endif %}
|
||||
|
||||
return renderLink(text, url);
|
||||
}
|
||||
},
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
{
|
||||
field: 'buttons',
|
||||
title: 'Actions',
|
||||
formatter: function(value, row) {
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||
{% endif %}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
},
|
||||
},
|
||||
{% endif %}
|
||||
]
|
||||
});
|
||||
|
||||
// Assign button callbacks to the newly created allocation buttons
|
||||
subTable.find(".button-allocation-edit").click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
launchModalForm(`/build/item/${pk}/edit/`, {
|
||||
success: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
subTable.find('.button-allocation-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
launchModalForm(`/build/item/${pk}/delete/`, {
|
||||
success: reloadTable,
|
||||
});
|
||||
});
|
||||
},
|
||||
formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; },
|
||||
onLoadSuccess: function(tableData) {
|
||||
// Once the BOM data are loaded, request allocation data for the build
|
||||
{% if build.status == BuildStatus.COMPLETE %}
|
||||
// Request StockItem which have been assigned to this build
|
||||
inventreeGet('/api/stock/',
|
||||
{
|
||||
build_order: {{ build.id }},
|
||||
location_detail: true,
|
||||
},
|
||||
{
|
||||
success: function(data) {
|
||||
// Iterate through the returned data, group by "part",
|
||||
var allocations = {};
|
||||
|
||||
data.forEach(function(item) {
|
||||
// Group allocations by referenced 'part'
|
||||
var key = parseInt(item.part);
|
||||
|
||||
if (!(key in allocations)) {
|
||||
allocations[key] = new Array();
|
||||
}
|
||||
|
||||
allocations[key].push(item);
|
||||
});
|
||||
|
||||
for (var key in allocations) {
|
||||
|
||||
var tableRow = buildTable.bootstrapTable('getRowByUniqueId', key);
|
||||
|
||||
tableRow.allocations = allocations[key];
|
||||
|
||||
buildTable.bootstrapTable('updateByUniqueId', key, tableRow, true);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
{% else %}
|
||||
inventreeGet('/api/build/item/',
|
||||
{
|
||||
build: {{ build.id }},
|
||||
},
|
||||
{
|
||||
success: function(data) {
|
||||
|
||||
// Iterate through the returned data, and group by "part"
|
||||
var allocations = {};
|
||||
|
||||
data.forEach(function(item) {
|
||||
|
||||
// Group allocations by referenced 'part'
|
||||
var part = item.part;
|
||||
var key = parseInt(part);
|
||||
|
||||
if (!(key in allocations)) {
|
||||
allocations[key] = new Array();
|
||||
}
|
||||
|
||||
// Add the allocation to the list
|
||||
allocations[key].push(item);
|
||||
});
|
||||
|
||||
for (var key in allocations) {
|
||||
|
||||
// Select the associated row in the table
|
||||
var tableRow = buildTable.bootstrapTable('getRowByUniqueId', key);
|
||||
|
||||
// Set the allocations for the row
|
||||
tableRow.allocations = allocations[key];
|
||||
|
||||
// And push the updated row back into the main table
|
||||
buildTable.bootstrapTable('updateByUniqueId', key, tableRow, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
{% endif %}
|
||||
},
|
||||
queryParams: {
|
||||
part: {{ build.part.id }},
|
||||
sub_part_detail: 1,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'sub_part',
|
||||
title: '{% trans "Part" %}',
|
||||
formatter: function(value, row, index, field) {
|
||||
return imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, `/part/${row.sub_part}/`);
|
||||
},
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'sub_part_detail.description',
|
||||
title: '{% trans "Description" %}',
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'reference',
|
||||
title: '{% trans "Reference" %}',
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Required" %}',
|
||||
formatter: function(value, row) {
|
||||
return value * {{ build.quantity }};
|
||||
},
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'allocated',
|
||||
{% if build.status == BuildStatus.COMPLETE %}
|
||||
title: '{% trans "Assigned" %}',
|
||||
{% else %}
|
||||
title: '{% trans "Allocated" %}',
|
||||
{% endif %}
|
||||
formatter: function(value, row) {
|
||||
|
||||
var allocated = sumAllocations(row);
|
||||
|
||||
return makeProgressBar(allocated, row.quantity * {{ build.quantity }});
|
||||
},
|
||||
sorter: function(valA, valB, rowA, rowB) {
|
||||
|
||||
var aA = sumAllocations(rowA);
|
||||
var aB = sumAllocations(rowB);
|
||||
|
||||
var qA = rowA.quantity * {{ build.quantity }};
|
||||
var qB = rowB.quantity * {{ build.quantity }};
|
||||
|
||||
if (aA == 0 && aB == 0) {
|
||||
return (qA > qB) ? 1 : -1;
|
||||
}
|
||||
|
||||
var progressA = parseFloat(aA) / qA;
|
||||
var progressB = parseFloat(aB) / qB;
|
||||
|
||||
return (progressA < progressB) ? 1 : -1;
|
||||
}
|
||||
},
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
{
|
||||
field: 'buttons',
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
var pk = row.sub_part;
|
||||
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
if (row.sub_part_detail.purchaseable) {
|
||||
html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy parts" %}');
|
||||
}
|
||||
|
||||
if (row.sub_part_detail.assembly) {
|
||||
html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}');
|
||||
}
|
||||
|
||||
html += makeIconButton('fa-plus icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
|
||||
{% endif %}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
},
|
||||
}
|
||||
{% endif %}
|
||||
],
|
||||
});
|
||||
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
$("#btn-allocate").on('click', function() {
|
||||
launchModalForm(
|
||||
@ -458,6 +84,14 @@ InvenTree | Allocate Parts
|
||||
);
|
||||
});
|
||||
|
||||
$('#btn-create-output').click(function() {
|
||||
launchModalForm('{% url "build-output-create" build.id %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#btn-order-parts").click(function() {
|
||||
launchModalForm("/order/purchase-order/order-parts/", {
|
||||
data: {
|
||||
|
20
InvenTree/build/templates/build/build_output_create.html
Normal file
20
InvenTree/build/templates/build/build_output_create.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% if build.part.has_trackable_parts %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "The Bill of Materials contains trackable parts" %}<br>
|
||||
{% trans "Build outputs must be generated individually." %}<br>
|
||||
{% trans "Multiple build outputs will be created based on the quantity specified." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if build.part.trackable %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "Trackable parts can have serial numbers specified" %}<br>
|
||||
{% trans "Enter serial numbers to generate multiple single build outputs" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -1,7 +1,7 @@
|
||||
{% extends "modal_form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block pre_form_content %}
|
||||
|
||||
Are you sure you wish to cancel this build?
|
||||
{% trans "Are you sure you wish to cancel this build?" %}
|
||||
|
||||
{% endblock %}
|
@ -1,48 +1,23 @@
|
||||
{% extends "modal_form.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% if fully_allocated %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<h4>{% trans "Stock allocation is complete" %}</h4>
|
||||
{% if build.can_complete %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Build can be completed" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
<h4>{% trans "Stock allocation is incomplete" %}</h4>
|
||||
|
||||
<div class='panel-group'>
|
||||
<div class='panel panel-default'>
|
||||
<div class='panel panel-heading'>
|
||||
<a data-toggle='collapse' href='#collapse-unallocated'>
|
||||
{{ unallocated_parts|length }} {% trans "parts have not been fully allocated" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class='panel-collapse collapse' id='collapse-unallocated'>
|
||||
<div class='panel-body'>
|
||||
<ul class='list-group'>
|
||||
{% for part in unallocated_parts %}
|
||||
<li class='list-group-item'>
|
||||
{% include "hover_image.html" with image=part.image %} {{ part }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
<b>{% trans "Build cannot be completed" %}</b><br>
|
||||
<ul>
|
||||
{% if build.incomplete_count > 0 %}
|
||||
<li>{% trans "Incompleted build outputs remain" %}</li>
|
||||
{% endif %}
|
||||
{% if build.completed < build.quantity %}
|
||||
<li>{% trans "Required build quantity has not been completed" %}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class='panel panel-info'>
|
||||
<div class='panel-heading'>
|
||||
{% trans "The following items will be created" %}
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
||||
{% decimal output.quantity %} x {{ output.part.full_name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
48
InvenTree/build/templates/build/complete_output.html
Normal file
48
InvenTree/build/templates/build/complete_output.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% extends "modal_form.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% if fully_allocated %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<h4>{% trans "Stock allocation is complete" %}</h4>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
<h4>{% trans "Stock allocation is incomplete" %}</h4>
|
||||
|
||||
<div class='panel-group'>
|
||||
<div class='panel panel-default'>
|
||||
<div class='panel panel-heading'>
|
||||
<a data-toggle='collapse' href='#collapse-unallocated'>
|
||||
{{ unallocated_parts|length }} {% trans "parts have not been fully allocated" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class='panel-collapse collapse' id='collapse-unallocated'>
|
||||
<div class='panel-body'>
|
||||
<ul class='list-group'>
|
||||
{% for part in unallocated_parts %}
|
||||
<li class='list-group-item'>
|
||||
{% include "hover_image.html" with image=part.image %} {{ part }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class='panel panel-info'>
|
||||
<div class='panel-heading'>
|
||||
{% trans "The following items will be created" %}
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
||||
{% decimal output.quantity %} x {{ output.part.full_name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -10,7 +10,7 @@ from stock.models import StockItem
|
||||
from part.models import Part, BomItem
|
||||
from InvenTree import status_codes as status
|
||||
|
||||
from InvenTree.helpers import ExtractSerialNumbers
|
||||
from InvenTree.helpers import extract_serial_numbers
|
||||
|
||||
|
||||
class BuildTest(TestCase):
|
||||
@ -188,7 +188,7 @@ class BuildTest(TestCase):
|
||||
self.assertTrue(self.build.isFullyAllocated())
|
||||
|
||||
# Generate some serial numbers!
|
||||
serials = ExtractSerialNumbers("1-10", 10)
|
||||
serials = extract_serial_numbers("1-10", 10)
|
||||
|
||||
self.build.completeBuild(None, serials, None)
|
||||
|
||||
|
@ -11,10 +11,12 @@ build_detail_urls = [
|
||||
url(r'^allocate/', views.BuildAllocate.as_view(), name='build-allocate'),
|
||||
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
||||
url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
|
||||
url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'),
|
||||
url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'),
|
||||
url(r'^complete/?', views.BuildComplete.as_view(), name='build-complete'),
|
||||
url(r'^complete-output/?', views.BuildOutputComplete.as_view(), name='build-output-complete'),
|
||||
url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'),
|
||||
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
|
||||
url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'),
|
||||
|
||||
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
||||
|
||||
|
@ -18,7 +18,7 @@ from stock.models import StockLocation, StockItem
|
||||
|
||||
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
|
||||
from InvenTree.views import InvenTreeRoleMixin
|
||||
from InvenTree.helpers import str2bool, ExtractSerialNumbers, normalize
|
||||
from InvenTree.helpers import str2bool, extract_serial_numbers, normalize
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
|
||||
@ -67,11 +67,13 @@ class BuildCancel(AjaxUpdateView):
|
||||
if not confirm:
|
||||
form.add_error('confirm_cancel', _('Confirm build cancellation'))
|
||||
|
||||
def post_save(self, build, form, **kwargs):
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Cancel the build.
|
||||
"""
|
||||
|
||||
build = self.get_object()
|
||||
|
||||
build.cancelBuild(self.request.user)
|
||||
|
||||
def get_data(self):
|
||||
@ -156,13 +158,12 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
if not output:
|
||||
form.add_error(None, _('Build output must be specified'))
|
||||
|
||||
def post_save(self, build, form, **kwargs):
|
||||
def save(self, build, form, **kwargs):
|
||||
"""
|
||||
Once the form has been validated,
|
||||
perform auto-allocations
|
||||
"""
|
||||
|
||||
build = self.get_object()
|
||||
output = form.cleaned_data.get('output', None)
|
||||
|
||||
build.autoAllocate(output)
|
||||
@ -173,6 +174,99 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
}
|
||||
|
||||
|
||||
class BuildOutputCreate(AjaxUpdateView):
|
||||
"""
|
||||
Create a new build output (StockItem) for a given build.
|
||||
"""
|
||||
|
||||
model = Build
|
||||
form_class = forms.BuildOutputCreateForm
|
||||
ajax_template_name = 'build/build_output_create.html'
|
||||
ajax_form_title = _('Create Build Output')
|
||||
role_required = 'build.change'
|
||||
|
||||
def validate(self, build, form, **kwargs):
|
||||
"""
|
||||
Validation for the form:
|
||||
"""
|
||||
|
||||
quantity = form.cleaned_data.get('quantity', None)
|
||||
serials = form.cleaned_data.get('serial_numbers', None)
|
||||
|
||||
# Check that the serial numbers are valid
|
||||
if serials:
|
||||
try:
|
||||
extracted = extract_serial_numbers(serials, quantity)
|
||||
|
||||
if extracted:
|
||||
# Check for conflicting serial numbers
|
||||
conflicts = build.part.find_conflicting_serial_numbers(extracted)
|
||||
|
||||
if len(conflicts) > 0:
|
||||
msg = ",".join([str(c) for c in conflicts])
|
||||
form.add_error(
|
||||
'serial_numbers',
|
||||
_('Serial numbers already exist') + ': ' + msg,
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
form.add_error('serial_numbers', e.messages)
|
||||
|
||||
else:
|
||||
# If no serial numbers are provided, should they be?
|
||||
if build.part.trackable:
|
||||
form.add_error('serial_numbers', _('Serial numbers required for trackable build output'))
|
||||
|
||||
def save(self, build, form, **kwargs):
|
||||
"""
|
||||
Create a new build output
|
||||
"""
|
||||
|
||||
data = form.cleaned_data
|
||||
|
||||
quantity = data.get('quantity', None)
|
||||
batch = data.get('batch', None)
|
||||
|
||||
serials = data.get('serial_numbers', None)
|
||||
|
||||
if serials:
|
||||
serial_numbers = extract_serial_numbers(serials, quantity)
|
||||
else:
|
||||
serial_numbers = None
|
||||
|
||||
build.create_build_output(
|
||||
quantity,
|
||||
serials=serial_numbers,
|
||||
batch=batch,
|
||||
)
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
build = self.get_object()
|
||||
|
||||
# Calculate the required quantity
|
||||
quantity = max(0, build.remaining - build.incomplete_count)
|
||||
initials['quantity'] = quantity
|
||||
|
||||
return initials
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
build = self.get_object()
|
||||
part = build.part
|
||||
|
||||
# If the part is not trackable, hide the serial number input
|
||||
if not part.trackable:
|
||||
form.fields['serial_numbers'] = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class BuildOutputDelete(AjaxUpdateView):
|
||||
"""
|
||||
Delete a build output (StockItem) for a given build.
|
||||
@ -182,7 +276,7 @@ class BuildOutputDelete(AjaxUpdateView):
|
||||
|
||||
model = Build
|
||||
form_class = forms.BuildOutputDeleteForm
|
||||
ajax_form_title = _('Delete build output')
|
||||
ajax_form_title = _('Delete Build Output')
|
||||
role_required = 'build.delete'
|
||||
|
||||
def get_initial(self):
|
||||
@ -296,7 +390,24 @@ class BuildUnallocate(AjaxUpdateView):
|
||||
|
||||
|
||||
class BuildComplete(AjaxUpdateView):
|
||||
""" View to mark a build as Complete.
|
||||
"""
|
||||
View to mark the build as complete.
|
||||
|
||||
Requirements:
|
||||
- There can be no outstanding build outputs
|
||||
- The "completed" value must meet or exceed the "quantity" value
|
||||
"""
|
||||
|
||||
model = Build
|
||||
form_class = forms.CompleteBuildForm
|
||||
role_required = 'build.change'
|
||||
ajax_form_title = _('Complete Build Order')
|
||||
ajax_template_name = 'build/complete.html'
|
||||
|
||||
|
||||
class BuildOutputComplete(AjaxUpdateView):
|
||||
"""
|
||||
View to mark a particular build output as Complete.
|
||||
|
||||
- Notifies the user of which parts will be removed from stock.
|
||||
- Removes allocated items from stock
|
||||
@ -304,10 +415,10 @@ class BuildComplete(AjaxUpdateView):
|
||||
"""
|
||||
|
||||
model = Build
|
||||
form_class = forms.CompleteBuildForm
|
||||
form_class = forms.CompleteBuildOutputForm
|
||||
context_object_name = "build"
|
||||
ajax_form_title = _("Complete Build Output")
|
||||
ajax_template_name = "build/complete.html"
|
||||
ajax_template_name = "build/complete_output.html"
|
||||
role_required = 'build.change'
|
||||
|
||||
def get_form(self):
|
||||
@ -422,7 +533,7 @@ class BuildComplete(AjaxUpdateView):
|
||||
|
||||
return context
|
||||
|
||||
def post_save(self, build, form, **kwargs):
|
||||
def save(self, build, form, **kwargs):
|
||||
|
||||
data = form.cleaned_data
|
||||
|
||||
@ -593,7 +704,7 @@ class BuildCreate(AjaxCreateView):
|
||||
|
||||
# Check that the provided serial numbers are sensible
|
||||
try:
|
||||
extracted = ExtractSerialNumbers(serials, quantity)
|
||||
extracted = extract_serial_numbers(serials, quantity)
|
||||
except ValidationError as e:
|
||||
extracted = None
|
||||
form.add_error('serial_numbers', e.messages)
|
||||
@ -912,9 +1023,14 @@ class BuildAttachmentCreate(AjaxCreateView):
|
||||
ajax_form_title = _('Add Build Order Attachment')
|
||||
role_required = 'build.add'
|
||||
|
||||
def post_save(self, **kwargs):
|
||||
self.object.user = self.request.user
|
||||
self.object.save()
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Add information on the user that uploaded the attachment
|
||||
"""
|
||||
|
||||
attachment = form.save(commit=False)
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
|
@ -100,7 +100,9 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView):
|
||||
ajax_template_name = "modal_form.html"
|
||||
role_required = 'purchase_order.add'
|
||||
|
||||
def post_save(self, attachment, form, **kwargs):
|
||||
def save(self, form, **kwargs):
|
||||
|
||||
attachment = form.save(commit=False)
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
@ -148,9 +150,14 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
|
||||
ajax_form_title = _('Add Sales Order Attachment')
|
||||
role_required = 'sales_order.add'
|
||||
|
||||
def post_save(self, attachment, form, **kwargs):
|
||||
self.object.user = self.request.user
|
||||
self.object.save()
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Save the user that uploaded the attachment
|
||||
"""
|
||||
|
||||
attachment = form.save(commit=False)
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -295,7 +302,9 @@ class SalesOrderNotes(InvenTreeRoleMixin, UpdateView):
|
||||
|
||||
|
||||
class PurchaseOrderCreate(AjaxCreateView):
|
||||
""" View for creating a new PurchaseOrder object using a modal form """
|
||||
"""
|
||||
View for creating a new PurchaseOrder object using a modal form
|
||||
"""
|
||||
|
||||
model = PurchaseOrder
|
||||
ajax_form_title = _("Create Purchase Order")
|
||||
@ -319,9 +328,12 @@ class PurchaseOrderCreate(AjaxCreateView):
|
||||
|
||||
return initials
|
||||
|
||||
def post_save(self, order, form, **kwargs):
|
||||
# Record the user who created this purchase order
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Record the user who created this PurchaseOrder
|
||||
"""
|
||||
|
||||
order = form.save(commit=False)
|
||||
order.created_by = self.request.user
|
||||
order.save()
|
||||
|
||||
@ -351,8 +363,12 @@ class SalesOrderCreate(AjaxCreateView):
|
||||
|
||||
return initials
|
||||
|
||||
def post_save(self, order, form, **kwargs):
|
||||
# Record the user who created this sales order
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Record the user who created this SalesOrder
|
||||
"""
|
||||
|
||||
order = form.save(commit=False)
|
||||
order.created_by = self.request.user
|
||||
order.save()
|
||||
|
||||
@ -414,7 +430,10 @@ class PurchaseOrderCancel(AjaxUpdateView):
|
||||
if not order.can_cancel():
|
||||
form.add_error(None, _('Order cannot be cancelled'))
|
||||
|
||||
def post_save(self, order, form, **kwargs):
|
||||
def save(self, order, form, **kwargs):
|
||||
"""
|
||||
Cancel the PurchaseOrder
|
||||
"""
|
||||
|
||||
order.cancel_order()
|
||||
|
||||
@ -438,7 +457,10 @@ class SalesOrderCancel(AjaxUpdateView):
|
||||
if not order.can_cancel():
|
||||
form.add_error(None, _('Order cannot be cancelled'))
|
||||
|
||||
def post_save(self, order, form, **kwargs):
|
||||
def save(self, order, form, **kwargs):
|
||||
"""
|
||||
Once the form has been validated, cancel the SalesOrder
|
||||
"""
|
||||
|
||||
order.cancel_order()
|
||||
|
||||
@ -459,8 +481,10 @@ class PurchaseOrderIssue(AjaxUpdateView):
|
||||
if not confirm:
|
||||
form.add_error('confirm', _('Confirm order placement'))
|
||||
|
||||
def post_save(self, order, form, **kwargs):
|
||||
|
||||
def save(self, order, form, **kwargs):
|
||||
"""
|
||||
Once the form has been validated, place the order.
|
||||
"""
|
||||
order.place_order()
|
||||
|
||||
def get_data(self):
|
||||
@ -495,7 +519,10 @@ class PurchaseOrderComplete(AjaxUpdateView):
|
||||
if not confirm:
|
||||
form.add_error('confirm', _('Confirm order completion'))
|
||||
|
||||
def post_save(self, order, form, **kwargs):
|
||||
def save(self, order, form, **kwargs):
|
||||
"""
|
||||
Complete the PurchaseOrder
|
||||
"""
|
||||
|
||||
order.complete_order()
|
||||
|
||||
|
@ -905,6 +905,7 @@ class Part(MPTTModel):
|
||||
def has_bom(self):
|
||||
return self.bom_count > 0
|
||||
|
||||
@property
|
||||
def has_trackable_parts(self):
|
||||
"""
|
||||
Return True if any parts linked in the Bill of Materials are trackable.
|
||||
|
@ -82,10 +82,14 @@ class PartAttachmentCreate(AjaxCreateView):
|
||||
|
||||
role_required = 'part.add'
|
||||
|
||||
def post_save(self):
|
||||
""" Record the user that uploaded the attachment """
|
||||
self.object.user = self.request.user
|
||||
self.object.save()
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Record the user that uploaded this attachment
|
||||
"""
|
||||
|
||||
attachment = form.save(commit=False)
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -874,7 +878,10 @@ class BomDuplicate(AjaxUpdateView):
|
||||
if not confirm:
|
||||
form.add_error('confirm', _('Confirm duplication of BOM from parent'))
|
||||
|
||||
def post_save(self, part, form):
|
||||
def save(self, part, form):
|
||||
"""
|
||||
Duplicate BOM from the specified parent
|
||||
"""
|
||||
|
||||
parent = form.cleaned_data.get('parent', None)
|
||||
|
||||
@ -915,7 +922,10 @@ class BomValidate(AjaxUpdateView):
|
||||
if not confirm:
|
||||
form.add_error('validate', _('Confirm that the BOM is valid'))
|
||||
|
||||
def post_save(self, part, form, **kwargs):
|
||||
def save(self, part, form, **kwargs):
|
||||
"""
|
||||
Mark the BOM as validated
|
||||
"""
|
||||
|
||||
part.validate_bom(self.request.user)
|
||||
|
||||
|
@ -21,7 +21,7 @@ from InvenTree.views import InvenTreeRoleMixin
|
||||
from InvenTree.forms import ConfirmForm
|
||||
|
||||
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
|
||||
from InvenTree.helpers import ExtractSerialNumbers
|
||||
from InvenTree.helpers import extract_serial_numbers
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from datetime import datetime
|
||||
@ -164,9 +164,10 @@ class StockItemAttachmentCreate(AjaxCreateView):
|
||||
ajax_template_name = "modal_form.html"
|
||||
role_required = 'stock.add'
|
||||
|
||||
def post_save(self, attachment, form, **kwargs):
|
||||
def save(self, form, **kwargs):
|
||||
""" Record the user that uploaded the attachment """
|
||||
|
||||
attachment = form.save(commit=False)
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
@ -252,7 +253,7 @@ class StockItemAssignToCustomer(AjaxUpdateView):
|
||||
if not customer:
|
||||
form.add_error('customer', _('Customer must be specified'))
|
||||
|
||||
def post_save(self, item, form, **kwargs):
|
||||
def save(self, item, form, **kwargs):
|
||||
"""
|
||||
Assign the stock item to the customer.
|
||||
"""
|
||||
@ -286,7 +287,7 @@ class StockItemReturnToStock(AjaxUpdateView):
|
||||
if not location:
|
||||
form.add_error('location', _('Specify a valid location'))
|
||||
|
||||
def post_save(self, item, form, **kwargs):
|
||||
def save(self, item, form, **kwargs):
|
||||
|
||||
location = form.cleaned_data.get('location', None)
|
||||
|
||||
@ -431,9 +432,12 @@ class StockItemTestResultCreate(AjaxCreateView):
|
||||
ajax_form_title = _("Add Test Result")
|
||||
role_required = 'stock.add'
|
||||
|
||||
def post_save(self, result, form, **kwargs):
|
||||
""" Record the user that uploaded the test result """
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Record the user that uploaded the test result
|
||||
"""
|
||||
|
||||
result = form.save(commit=False)
|
||||
result.user = self.request.user
|
||||
result.save()
|
||||
|
||||
@ -1405,7 +1409,7 @@ class StockItemSerialize(AjaxUpdateView):
|
||||
destination = None
|
||||
|
||||
try:
|
||||
numbers = ExtractSerialNumbers(serials, quantity)
|
||||
numbers = extract_serial_numbers(serials, quantity)
|
||||
except ValidationError as e:
|
||||
form.add_error('serial_numbers', e.messages)
|
||||
valid = False
|
||||
@ -1630,7 +1634,7 @@ class StockItemCreate(AjaxCreateView):
|
||||
# If user has specified a range of serial numbers
|
||||
if len(sn) > 0:
|
||||
try:
|
||||
serials = ExtractSerialNumbers(sn, quantity)
|
||||
serials = extract_serial_numbers(sn, quantity)
|
||||
|
||||
existing = part.find_conflicting_serial_numbers(serials)
|
||||
|
||||
|
@ -92,14 +92,14 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
data: {
|
||||
output: outputId,
|
||||
},
|
||||
success: reloadTable,
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(panel).find(`#button-output-complete-${outputId}`).click(function() {
|
||||
launchModalForm(
|
||||
`/build/${buildId}/complete/`,
|
||||
`/build/${buildId}/complete-output/`,
|
||||
{
|
||||
success: reloadTable,
|
||||
data: {
|
||||
|
Loading…
Reference in New Issue
Block a user