mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Adds a BomUpload endpoint to handle upload of complete BOM
This commit is contained in:
parent
80818c464a
commit
a2c48d308f
@ -2,7 +2,7 @@
|
||||
Custom field validators for InvenTree
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -138,7 +138,7 @@ def validate_overage(value):
|
||||
|
||||
# Looks like a number
|
||||
return True
|
||||
except ValueError:
|
||||
except (ValueError, InvalidOperation):
|
||||
pass
|
||||
|
||||
# Now look for a percentage value
|
||||
@ -159,7 +159,7 @@ def validate_overage(value):
|
||||
pass
|
||||
|
||||
raise ValidationError(
|
||||
_("Overage must be an integer value or a percentage")
|
||||
_("Invalid value for overage")
|
||||
)
|
||||
|
||||
|
||||
|
@ -1545,7 +1545,7 @@ class BomExtract(generics.CreateAPIView):
|
||||
"""
|
||||
Custom create function to return the extracted data
|
||||
"""
|
||||
|
||||
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
@ -1556,6 +1556,16 @@ class BomExtract(generics.CreateAPIView):
|
||||
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
|
||||
class BomUpload(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for uploading a complete Bill of Materials.
|
||||
|
||||
It is assumed that the BOM has been extracted from a file using the BomExtract endpoint.
|
||||
"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = part_serializers.BomUploadSerializer
|
||||
|
||||
|
||||
class BomDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a single BomItem object """
|
||||
@ -1710,6 +1720,9 @@ bom_api_urls = [
|
||||
])),
|
||||
|
||||
url(r'^extract/', BomExtract.as_view(), name='api-bom-extract'),
|
||||
|
||||
url(r'^upload/', BomUpload.as_view(), name='api-bom-upload'),
|
||||
|
||||
# Catch-all
|
||||
url(r'^.*$', BomList.as_view(), name='api-bom-list'),
|
||||
]
|
||||
|
@ -8,7 +8,7 @@ import os
|
||||
import tablib
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -465,7 +465,13 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
price_range = serializers.CharField(read_only=True)
|
||||
|
||||
quantity = InvenTreeDecimalField()
|
||||
quantity = InvenTreeDecimalField(required=True)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
if quantity <= 0:
|
||||
raise serializers.ValidationError(_("Quantity must be greater than zero"))
|
||||
|
||||
return quantity
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))
|
||||
|
||||
@ -927,4 +933,37 @@ class BomExtractSerializer(serializers.Serializer):
|
||||
"""
|
||||
There is no action associated with "saving" this serializer
|
||||
"""
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
class BomUploadSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for uploading a BOM against a specified part.
|
||||
|
||||
A "BOM" is a set of BomItem objects which are to be validated together as a set
|
||||
"""
|
||||
|
||||
items = BomItemSerializer(many=True, required=True)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
items = data['items']
|
||||
|
||||
if len(items) == 0:
|
||||
raise serializers.ValidationError(_("At least one BOM item is required"))
|
||||
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
items = data['items']
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
for item in items:
|
||||
print(item)
|
||||
|
@ -23,6 +23,7 @@
|
||||
loadUsedInTable,
|
||||
removeRowFromBomWizard,
|
||||
removeColFromBomWizard,
|
||||
submitBomTable
|
||||
*/
|
||||
|
||||
|
||||
@ -41,6 +42,7 @@ function constructBomUploadTable(data, options={}) {
|
||||
|
||||
var field_options = {
|
||||
hideLabels: true,
|
||||
hideClearButton: true,
|
||||
};
|
||||
|
||||
function constructRowField(field_name) {
|
||||
@ -53,7 +55,7 @@ function constructBomUploadTable(data, options={}) {
|
||||
|
||||
field.value = row[field_name];
|
||||
|
||||
return constructField(`${field_name}_${idx}`, field, field_options);
|
||||
return constructField(`items_${field_name}_${idx}`, field, field_options);
|
||||
|
||||
}
|
||||
|
||||
@ -75,8 +77,7 @@ function constructBomUploadTable(data, options={}) {
|
||||
buttons += `</div>`;
|
||||
|
||||
var html = `
|
||||
<tr id='bom_import_row_${idx}' class='bom-import-row'>
|
||||
<td id='col_buttons_${idx}'>${buttons}</td>
|
||||
<tr id='bom_import_row_${idx}' class='bom-import-row' idx='${idx}'>
|
||||
<td id='col_sub_part_${idx}'>${sub_part}</td>
|
||||
<td id='col_quantity_${idx}'>${quantity}</td>
|
||||
<td id='col_reference_${idx}'>${reference}</td>
|
||||
@ -85,6 +86,7 @@ function constructBomUploadTable(data, options={}) {
|
||||
<td id='col_inherited_${idx}'>${inherited}</td>
|
||||
<td id='col_optional_${idx}'>${optional}</td>
|
||||
<td id='col_note_${idx}'>${note}</td>
|
||||
<td id='col_buttons_${idx}'>${buttons}</td>
|
||||
</tr>`;
|
||||
|
||||
$('#bom-import-table tbody').append(html);
|
||||
@ -92,7 +94,7 @@ function constructBomUploadTable(data, options={}) {
|
||||
// Initialize the "part" selector for this row
|
||||
initializeRelatedField(
|
||||
{
|
||||
name: `sub_part_${idx}`,
|
||||
name: `items_sub_part_${idx}`,
|
||||
value: row.part,
|
||||
api_url: '{% url "api-part-list" %}',
|
||||
filters: {
|
||||
@ -111,15 +113,6 @@ function constructBomUploadTable(data, options={}) {
|
||||
$(`#button-row-remove-${idx}`).click(function() {
|
||||
$(`#bom_import_row_${idx}`).remove();
|
||||
});
|
||||
|
||||
// Add callbacks for the fields which allow it
|
||||
function addRowClearCallback(field_name) {
|
||||
addClearCallback(`${field_name}_${idx}`, fields[field_name]);
|
||||
}
|
||||
|
||||
addRowClearCallback('reference');
|
||||
addRowClearCallback('overage');
|
||||
addRowClearCallback('note');
|
||||
}
|
||||
|
||||
// Request API endpoint options
|
||||
@ -134,6 +127,70 @@ function constructBomUploadTable(data, options={}) {
|
||||
}
|
||||
|
||||
|
||||
/* Extract rows from the BOM upload table,
|
||||
* and submit data to the server
|
||||
*/
|
||||
function submitBomTable(part_id, options={}) {
|
||||
|
||||
// Extract rows from the form
|
||||
var rows = [];
|
||||
|
||||
var idx_values = [];
|
||||
|
||||
var url = '{% url "api-bom-upload" %}';
|
||||
|
||||
$('.bom-import-row').each(function() {
|
||||
var idx = $(this).attr('idx');
|
||||
|
||||
idx_values.push(idx);
|
||||
|
||||
// Extract each field from the row
|
||||
rows.push({
|
||||
part: part_id,
|
||||
sub_part: getFormFieldValue(`items_sub_part_${idx}`, {}),
|
||||
quantity: getFormFieldValue(`items_quantity_${idx}`, {}),
|
||||
reference: getFormFieldValue(`items_reference_${idx}`, {}),
|
||||
overage: getFormFieldValue(`items_overage_${idx}`, {}),
|
||||
allow_variants: getFormFieldValue(`items_allow_variants_${idx}`, {type: 'boolean'}),
|
||||
inherited: getFormFieldValue(`items_inherited_${idx}`, {type: 'boolean'}),
|
||||
optional: getFormFieldValue(`items_optional_${idx}`, {type: 'boolean'}),
|
||||
note: getFormFieldValue(`items_note_${idx}`, {}),
|
||||
})
|
||||
});
|
||||
|
||||
var data = {
|
||||
items: rows,
|
||||
};
|
||||
|
||||
var options = {
|
||||
nested: {
|
||||
items: idx_values,
|
||||
}
|
||||
};
|
||||
|
||||
getApiEndpointOptions(url, function(response) {
|
||||
var fields = response.actions.POST;
|
||||
|
||||
inventreePut(url, data, {
|
||||
method: 'POST',
|
||||
success: function(response) {
|
||||
// TODO: Return to the "bom" page
|
||||
},
|
||||
error: function(xhr) {
|
||||
switch (xhr.status) {
|
||||
case 400:
|
||||
handleFormErrors(xhr.responseJSON, fields, options);
|
||||
break;
|
||||
default:
|
||||
showApiError(xhr, url);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function downloadBomTemplate(options={}) {
|
||||
|
||||
var format = options.format;
|
||||
|
@ -890,12 +890,13 @@ function validateFormField(name, options) {
|
||||
* - field: The field specification provided from the OPTIONS request
|
||||
* - options: The original options object provided by the client
|
||||
*/
|
||||
function getFormFieldValue(name, field, options) {
|
||||
function getFormFieldValue(name, field={}, options={}) {
|
||||
|
||||
// Find the HTML element
|
||||
var el = getFormFieldElement(name, options);
|
||||
|
||||
if (!el) {
|
||||
console.log(`ERROR: getFormFieldValue could not locate field '{name}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -981,16 +982,22 @@ function handleFormSuccess(response, options) {
|
||||
/*
|
||||
* Remove all error text items from the form
|
||||
*/
|
||||
function clearFormErrors(options) {
|
||||
function clearFormErrors(options={}) {
|
||||
|
||||
// Remove the individual error messages
|
||||
$(options.modal).find('.form-error-message').remove();
|
||||
if (options && options.modal) {
|
||||
// Remove the individual error messages
|
||||
$(options.modal).find('.form-error-message').remove();
|
||||
|
||||
// Remove the "has error" class
|
||||
$(options.modal).find('.form-field-error').removeClass('form-field-error');
|
||||
// Remove the "has error" class
|
||||
$(options.modal).find('.form-field-error').removeClass('form-field-error');
|
||||
|
||||
// Hide the 'non field errors'
|
||||
$(options.modal).find('#non-field-errors').html('');
|
||||
// Hide the 'non field errors'
|
||||
$(options.modal).find('#non-field-errors').html('');
|
||||
} else {
|
||||
$('.form-error-message').remove();
|
||||
$('.form-field-errors').removeClass('form-field-error');
|
||||
$('#non-field-errors').html('');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -1018,7 +1025,7 @@ function clearFormErrors(options) {
|
||||
*
|
||||
*/
|
||||
|
||||
function handleNestedErrors(errors, field_name, options) {
|
||||
function handleNestedErrors(errors, field_name, options={}) {
|
||||
|
||||
var error_list = errors[field_name];
|
||||
|
||||
@ -1074,15 +1081,23 @@ function handleNestedErrors(errors, field_name, options) {
|
||||
* - fields: The form data object
|
||||
* - options: Form options provided by the client
|
||||
*/
|
||||
function handleFormErrors(errors, fields, options) {
|
||||
function handleFormErrors(errors, fields={}, options={}) {
|
||||
|
||||
// Reset the status of the "submit" button
|
||||
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
||||
if (options.modal) {
|
||||
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
||||
}
|
||||
|
||||
// Remove any existing error messages from the form
|
||||
clearFormErrors(options);
|
||||
|
||||
var non_field_errors = $(options.modal).find('#non-field-errors');
|
||||
var non_field_errors = null;
|
||||
|
||||
if (options.modal) {
|
||||
non_field_errors = $(options.modal).find('#non-field-errors');
|
||||
} else {
|
||||
non_field_errors = $('#non-field-errors');
|
||||
}
|
||||
|
||||
// TODO: Display the JSON error text when hovering over the "info" icon
|
||||
non_field_errors.append(
|
||||
@ -1158,14 +1173,19 @@ function handleFormErrors(errors, fields, options) {
|
||||
/*
|
||||
* Add a rendered error message to the provided field
|
||||
*/
|
||||
function addFieldErrorMessage(name, error_text, error_idx, options) {
|
||||
function addFieldErrorMessage(name, error_text, error_idx, options={}) {
|
||||
|
||||
field_name = getFieldName(name, options);
|
||||
|
||||
// Add the 'form-field-error' class
|
||||
$(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error');
|
||||
var field_dom = null;
|
||||
|
||||
var field_dom = $(options.modal).find(`#errors-${field_name}`);
|
||||
if (options.modal) {
|
||||
$(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error');
|
||||
field_dom = $(options.modal).find(`#errors-${field_name}`);
|
||||
} else {
|
||||
$(`#div_id_${field_name}`).addClass('form-field-error');
|
||||
field_dom = $(`#errors-${field_name}`);
|
||||
}
|
||||
|
||||
if (field_dom) {
|
||||
|
||||
@ -1492,17 +1512,20 @@ function initializeRelatedField(field, fields, options={}) {
|
||||
|
||||
var parent = null;
|
||||
var auto_width = false;
|
||||
var width = '100%';
|
||||
|
||||
// Special considerations if the select2 input is a child of a modal
|
||||
if (options && options.modal) {
|
||||
parent = $(options.modal);
|
||||
auto_width = true;
|
||||
width = null;
|
||||
}
|
||||
|
||||
select.select2({
|
||||
placeholder: '',
|
||||
dropdownParent: parent,
|
||||
dropdownAutoWidth: auto_width,
|
||||
width: width,
|
||||
language: {
|
||||
noResults: function(query) {
|
||||
if (field.noResults) {
|
||||
@ -1949,7 +1972,7 @@ function constructField(name, parameters, options) {
|
||||
|
||||
if (extra) {
|
||||
|
||||
if (!parameters.required) {
|
||||
if (!parameters.required && !options.hideClearButton) {
|
||||
html += `
|
||||
<span class='input-group-text form-clear' id='clear_${field_name}' title='{% trans "Clear input" %}'>
|
||||
<span class='icon-red fas fa-backspace'></span>
|
||||
|
Loading…
Reference in New Issue
Block a user