mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2770 from SchrodingersGat/order-parts-wizard
Order parts wizard
This commit is contained in:
commit
e938870b32
@ -312,7 +312,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
||||
try:
|
||||
params = self.request.query_params
|
||||
kwargs['part_detail'] = str2bool(params.get('part_detail', None))
|
||||
kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', None))
|
||||
kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', True))
|
||||
kwargs['manufacturer_detail'] = str2bool(params.get('manufacturer_detail', None))
|
||||
kwargs['pretty'] = str2bool(params.get('pretty', None))
|
||||
except AttributeError:
|
||||
|
@ -223,14 +223,30 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
if order_detail is not True:
|
||||
self.fields.pop('order_detail')
|
||||
|
||||
quantity = serializers.FloatField(default=1)
|
||||
received = serializers.FloatField(default=0)
|
||||
quantity = serializers.FloatField(min_value=0, required=True)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_("Quantity must be greater than zero"))
|
||||
|
||||
return quantity
|
||||
|
||||
def validate_purchase_order(self, purchase_order):
|
||||
|
||||
if purchase_order.status not in PurchaseOrderStatus.OPEN:
|
||||
raise ValidationError(_('Order is not open'))
|
||||
|
||||
return purchase_order
|
||||
|
||||
received = serializers.FloatField(default=0, read_only=True)
|
||||
|
||||
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
total_price = serializers.FloatField(read_only=True)
|
||||
|
||||
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
|
||||
|
||||
supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
|
||||
|
||||
purchase_price = InvenTreeMoneySerializer(
|
||||
@ -248,6 +264,32 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
supplier_part = data.get('part', None)
|
||||
purchase_order = data.get('order', None)
|
||||
|
||||
if not supplier_part:
|
||||
raise ValidationError({
|
||||
'part': _('Supplier part must be specified'),
|
||||
})
|
||||
|
||||
if not purchase_order:
|
||||
raise ValidationError({
|
||||
'order': _('Purchase order must be specified'),
|
||||
})
|
||||
|
||||
# Check that the supplier part and purchase order match
|
||||
if supplier_part is not None and supplier_part.supplier != purchase_order.supplier:
|
||||
raise ValidationError({
|
||||
'part': _('Supplier must match purchase order'),
|
||||
'order': _('Purchase order must match supplier'),
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = order.models.PurchaseOrderLineItem
|
||||
|
||||
|
@ -169,13 +169,18 @@
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
{% if roles.part.change %}
|
||||
<li><a class='dropdown-item' href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='multi-part-category' title='{% trans "Set category" %}'>
|
||||
<span class='fas fa-sitemap'></span> {% trans "Set Category" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li><a class='dropdown-item' href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='multi-part-order' title='{% trans "Order parts" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
||||
</a></li>
|
||||
{% if report_enabled %}
|
||||
<li><a class='dropdown-item' href='#' id='multi-part-print-label' title='{% trans "Print Labels" %}'>{% trans "Print Labels" %}</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='multi-part-print-label' title='{% trans "Print Labels" %}'>
|
||||
<span class='fas fa-tag'></span> {% trans "Print Labels" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li><a class='dropdown-item' href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% include "filter_list.html" with id="parts" %}
|
||||
|
@ -536,6 +536,22 @@
|
||||
{% endif %}
|
||||
|
||||
$("#part-order").click(function() {
|
||||
|
||||
inventreeGet(
|
||||
'{% url "api-part-detail" part.pk %}',
|
||||
{},
|
||||
{
|
||||
success: function(part) {
|
||||
orderParts(
|
||||
[part],
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
launchModalForm("{% url 'order-parts' %}", {
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
|
@ -105,7 +105,7 @@ function inventreeFormDataUpload(url, data, options={}) {
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log('Form data upload failure: ' + status);
|
||||
console.error('Form data upload failure: ' + status);
|
||||
|
||||
if (options.error) {
|
||||
options.error(xhr, status, error);
|
||||
|
@ -86,7 +86,6 @@ function onCameraAvailable(hasCamera, options) {
|
||||
|
||||
function onBarcodeScanCompleted(result, options) {
|
||||
if (result.data == '') return;
|
||||
console.log('decoded qr code:', result.data);
|
||||
stopQrScanner();
|
||||
postBarcodeData(result.data, options);
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ function constructBomUploadTable(data, options={}) {
|
||||
|
||||
var modal = createNewModal({
|
||||
title: '{% trans "Row Data" %}',
|
||||
cancelText: '{% trans "Close" %}',
|
||||
closeText: '{% trans "Close" %}',
|
||||
hideSubmitButton: true
|
||||
});
|
||||
|
||||
@ -617,7 +617,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
||||
},
|
||||
},
|
||||
preFormContent: html,
|
||||
cancelText: '{% trans "Close" %}',
|
||||
closeText: '{% trans "Close" %}',
|
||||
submitText: '{% trans "Add Substitute" %}',
|
||||
title: '{% trans "Edit BOM Item Substitutes" %}',
|
||||
afterRender: function(fields, opts) {
|
||||
@ -1061,7 +1061,7 @@ function loadBomTable(table, options={}) {
|
||||
table.bootstrapTable('append', response);
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.log('Error requesting BOM for part=' + part_pk);
|
||||
console.error('Error requesting BOM for part=' + part_pk);
|
||||
showApiError(xhr);
|
||||
}
|
||||
}
|
||||
|
@ -115,10 +115,6 @@ function supplierPartFields() {
|
||||
|
||||
return {
|
||||
part: {},
|
||||
supplier: {},
|
||||
SKU: {
|
||||
icon: 'fa-hashtag',
|
||||
},
|
||||
manufacturer_part: {
|
||||
filters: {
|
||||
part_detail: true,
|
||||
@ -126,6 +122,10 @@ function supplierPartFields() {
|
||||
},
|
||||
auto_fill: true,
|
||||
},
|
||||
supplier: {},
|
||||
SKU: {
|
||||
icon: 'fa-hashtag',
|
||||
},
|
||||
description: {},
|
||||
link: {
|
||||
icon: 'fa-link',
|
||||
|
@ -62,7 +62,7 @@ function loadTableFilters(tableKey) {
|
||||
if (f.length == 2) {
|
||||
filters[f[0]] = f[1];
|
||||
} else {
|
||||
console.log(`Improperly formatted filter: ${item}`);
|
||||
console.warn(`Improperly formatted filter: ${item}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -274,7 +274,7 @@ function setupFilterList(tableKey, table, target, options={}) {
|
||||
var element = $(target);
|
||||
|
||||
if (!element || !element.exists()) {
|
||||
console.log(`WARNING: setupFilterList could not find target '${target}'`);
|
||||
console.warn(`setupFilterList could not find target '${target}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ function getApiEndpointOptions(url, callback) {
|
||||
success: callback,
|
||||
error: function(xhr) {
|
||||
// TODO: Handle error
|
||||
console.log(`ERROR in getApiEndpointOptions at '${url}'`);
|
||||
console.error(`Error in getApiEndpointOptions at '${url}'`);
|
||||
showApiError(xhr, url);
|
||||
}
|
||||
});
|
||||
@ -227,7 +227,7 @@ function constructChangeForm(fields, options) {
|
||||
},
|
||||
error: function(xhr) {
|
||||
// TODO: Handle error here
|
||||
console.log(`ERROR in constructChangeForm at '${options.url}'`);
|
||||
console.error(`Error in constructChangeForm at '${options.url}'`);
|
||||
|
||||
showApiError(xhr, options.url);
|
||||
}
|
||||
@ -268,7 +268,7 @@ function constructDeleteForm(fields, options) {
|
||||
},
|
||||
error: function(xhr) {
|
||||
// TODO: Handle error here
|
||||
console.log(`ERROR in constructDeleteForm at '${options.url}`);
|
||||
console.error(`Error in constructDeleteForm at '${options.url}`);
|
||||
|
||||
showApiError(xhr, options.url);
|
||||
}
|
||||
@ -354,7 +354,7 @@ function constructForm(url, options) {
|
||||
icon: 'fas fa-user-times',
|
||||
});
|
||||
|
||||
console.log(`'POST action unavailable at ${url}`);
|
||||
console.warn(`'POST action unavailable at ${url}`);
|
||||
}
|
||||
break;
|
||||
case 'PUT':
|
||||
@ -369,7 +369,7 @@ function constructForm(url, options) {
|
||||
icon: 'fas fa-user-times',
|
||||
});
|
||||
|
||||
console.log(`${options.method} action unavailable at ${url}`);
|
||||
console.warn(`${options.method} action unavailable at ${url}`);
|
||||
}
|
||||
break;
|
||||
case 'DELETE':
|
||||
@ -383,7 +383,7 @@ function constructForm(url, options) {
|
||||
icon: 'fas fa-user-times',
|
||||
});
|
||||
|
||||
console.log(`DELETE action unavailable at ${url}`);
|
||||
console.warn(`DELETE action unavailable at ${url}`);
|
||||
}
|
||||
break;
|
||||
case 'GET':
|
||||
@ -397,11 +397,11 @@ function constructForm(url, options) {
|
||||
icon: 'fas fa-user-times',
|
||||
});
|
||||
|
||||
console.log(`GET action unavailable at ${url}`);
|
||||
console.warn(`GET action unavailable at ${url}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log(`constructForm() called with invalid method '${options.method}'`);
|
||||
console.warn(`constructForm() called with invalid method '${options.method}'`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@ -731,7 +731,7 @@ function submitFormData(fields, options) {
|
||||
data[name] = value;
|
||||
}
|
||||
} else {
|
||||
console.log(`WARNING: Could not find field matching '${name}'`);
|
||||
console.warn(`Could not find field matching '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -776,7 +776,7 @@ function submitFormData(fields, options) {
|
||||
default:
|
||||
$(options.modal).modal('hide');
|
||||
|
||||
console.log(`upload error at ${options.url}`);
|
||||
console.error(`Upload error at ${options.url}`);
|
||||
showApiError(xhr, options.url);
|
||||
break;
|
||||
}
|
||||
@ -827,7 +827,7 @@ function updateFieldValue(name, value, field, options) {
|
||||
var el = getFormFieldElement(name, options);
|
||||
|
||||
if (!el) {
|
||||
console.log(`WARNING: updateFieldValue could not find field '${name}'`);
|
||||
console.warn(`updateFieldValue could not find field '${name}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -870,7 +870,7 @@ function getFormFieldElement(name, options) {
|
||||
}
|
||||
|
||||
if (!el.exists) {
|
||||
console.log(`ERROR: Could not find form element for field '${name}'`);
|
||||
console.error(`Could not find form element for field '${name}'`);
|
||||
}
|
||||
|
||||
return el;
|
||||
@ -918,7 +918,7 @@ function getFormFieldValue(name, field={}, options={}) {
|
||||
var el = getFormFieldElement(name, options);
|
||||
|
||||
if (!el.exists()) {
|
||||
console.log(`ERROR: getFormFieldValue could not locate field '${name}'`);
|
||||
console.error(`getFormFieldValue could not locate field '${name}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1104,7 +1104,7 @@ function handleNestedErrors(errors, field_name, options={}) {
|
||||
|
||||
// Nest list must be provided!
|
||||
if (!nest_list) {
|
||||
console.log(`WARNING: handleNestedErrors missing nesting options for field '${fieldName}'`);
|
||||
console.warn(`handleNestedErrors missing nesting options for field '${fieldName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1113,7 +1113,7 @@ function handleNestedErrors(errors, field_name, options={}) {
|
||||
var error_item = error_list[idx];
|
||||
|
||||
if (idx >= nest_list.length) {
|
||||
console.log(`WARNING: handleNestedErrors returned greater number of errors (${error_list.length}) than could be handled (${nest_list.length})`);
|
||||
console.warn(`handleNestedErrors returned greater number of errors (${error_list.length}) than could be handled (${nest_list.length})`);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1218,29 +1218,26 @@ function handleFormErrors(errors, fields={}, options={}) {
|
||||
|
||||
for (var field_name in errors) {
|
||||
|
||||
if (field_name in fields) {
|
||||
var field = fields[field_name] || {};
|
||||
|
||||
var field = fields[field_name];
|
||||
if ((field.type == 'field') && ('child' in field)) {
|
||||
// This is a "nested" field
|
||||
handleNestedErrors(errors, field_name, options);
|
||||
} else {
|
||||
// This is a "simple" field
|
||||
|
||||
if ((field.type == 'field') && ('child' in field)) {
|
||||
// This is a "nested" field
|
||||
handleNestedErrors(errors, field_name, options);
|
||||
} else {
|
||||
// This is a "simple" field
|
||||
var field_errors = errors[field_name];
|
||||
|
||||
var field_errors = errors[field_name];
|
||||
if (field_errors && !first_error_field && isFieldVisible(field_name, options)) {
|
||||
first_error_field = field_name;
|
||||
}
|
||||
|
||||
if (field_errors && !first_error_field && isFieldVisible(field_name, options)) {
|
||||
first_error_field = field_name;
|
||||
}
|
||||
// Add an entry for each returned error message
|
||||
for (var ii = field_errors.length-1; ii >= 0; ii--) {
|
||||
|
||||
// Add an entry for each returned error message
|
||||
for (var ii = field_errors.length-1; ii >= 0; ii--) {
|
||||
var error_text = field_errors[ii];
|
||||
|
||||
var error_text = field_errors[ii];
|
||||
|
||||
addFieldErrorMessage(field_name, error_text, ii, options);
|
||||
}
|
||||
addFieldErrorMessage(field_name, error_text, ii, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1285,7 +1282,7 @@ function addFieldErrorMessage(name, error_text, error_idx=0, options={}) {
|
||||
|
||||
field_dom.append(error_html);
|
||||
} else {
|
||||
console.log(`WARNING: addFieldErrorMessage could not locate field '${field_name}'`);
|
||||
console.warn(`addFieldErrorMessage could not locate field '${field_name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1358,7 +1355,7 @@ function addClearCallback(name, field, options={}) {
|
||||
}
|
||||
|
||||
if (!el) {
|
||||
console.log(`WARNING: addClearCallback could not find field '${name}'`);
|
||||
console.warn(`addClearCallback could not find field '${name}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1582,7 +1579,7 @@ function initializeRelatedField(field, fields, options={}) {
|
||||
var name = field.name;
|
||||
|
||||
if (!field.api_url) {
|
||||
console.log(`WARNING: Related field '${name}' missing 'api_url' parameter.`);
|
||||
console.warn(`Related field '${name}' missing 'api_url' parameter.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1712,7 +1709,7 @@ function initializeRelatedField(field, fields, options={}) {
|
||||
return $(html);
|
||||
} else {
|
||||
// Return a simple renderering
|
||||
console.log(`WARNING: templateResult() missing 'field.model' for '${name}'`);
|
||||
console.warn(`templateResult() missing 'field.model' for '${name}'`);
|
||||
return `${name} - ${item.id}`;
|
||||
}
|
||||
},
|
||||
@ -1742,7 +1739,7 @@ function initializeRelatedField(field, fields, options={}) {
|
||||
return $(html);
|
||||
} else {
|
||||
// Return a simple renderering
|
||||
console.log(`WARNING: templateSelection() missing 'field.model' for '${name}'`);
|
||||
console.warn(`templateSelection() missing 'field.model' for '${name}'`);
|
||||
return `${name} - ${item.id}`;
|
||||
}
|
||||
}
|
||||
@ -1780,6 +1777,11 @@ function initializeRelatedField(field, fields, options={}) {
|
||||
// Only a single result is available, given the provided filters
|
||||
if (data.count == 1) {
|
||||
setRelatedFieldData(name, data.results[0], options);
|
||||
|
||||
// Run "callback" function (if supplied)
|
||||
if (field.onEdit) {
|
||||
field.onEdit(data.results[0], name, field, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1911,7 +1913,7 @@ function renderModelData(name, model, data, parameters, options) {
|
||||
if (html != null) {
|
||||
return html;
|
||||
} else {
|
||||
console.log(`ERROR: Rendering not implemented for model '${model}'`);
|
||||
console.error(`Rendering not implemented for model '${model}'`);
|
||||
// Simple text rendering
|
||||
return `${model} - ID ${data.id}`;
|
||||
}
|
||||
@ -1924,6 +1926,10 @@ function renderModelData(name, model, data, parameters, options) {
|
||||
function getFieldName(name, options={}) {
|
||||
var field_name = name;
|
||||
|
||||
if (options.field_suffix) {
|
||||
field_name += options.field_suffix;
|
||||
}
|
||||
|
||||
if (options && options.depth) {
|
||||
field_name += `_${options.depth}`;
|
||||
}
|
||||
@ -2196,7 +2202,7 @@ function constructInput(name, parameters, options={}) {
|
||||
if (func != null) {
|
||||
html = func(name, parameters, options);
|
||||
} else {
|
||||
console.log(`WARNING: Unhandled form field type: '${parameters.type}'`);
|
||||
console.warn(`Unhandled form field type: '${parameters.type}'`);
|
||||
}
|
||||
|
||||
return html;
|
||||
@ -2499,12 +2505,12 @@ function constructHelpText(name, parameters) {
|
||||
function selectImportFields(url, data={}, options={}) {
|
||||
|
||||
if (!data.model_fields) {
|
||||
console.log(`WARNING: selectImportFields is missing 'model_fields'`);
|
||||
console.warn(`selectImportFields is missing 'model_fields'`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.file_fields) {
|
||||
console.log(`WARNING: selectImportFields is missing 'file_fields'`);
|
||||
console.warn(`selectImportFields is missing 'file_fields'`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2595,7 +2601,7 @@ function selectImportFields(url, data={}, options={}) {
|
||||
default:
|
||||
$(opts.modal).modal('hide');
|
||||
|
||||
console.log(`upload error at ${opts.url}`);
|
||||
console.error(`upload error at ${opts.url}`);
|
||||
showApiError(xhr, opts.url);
|
||||
break;
|
||||
}
|
||||
|
@ -85,12 +85,25 @@ function createNewModal(options={}) {
|
||||
|
||||
var modal_name = `#modal-form-${id}`;
|
||||
|
||||
// Callback *after* the modal has been rendered
|
||||
$(modal_name).on('shown.bs.modal', function() {
|
||||
$(modal_name + ' .modal-form-content').scrollTop(0);
|
||||
|
||||
if (options.focus) {
|
||||
getFieldByName(modal_name, options.focus).focus();
|
||||
}
|
||||
|
||||
// Steal keyboard focus
|
||||
$(modal_name).focus();
|
||||
|
||||
if (options.hideCloseButton) {
|
||||
$(modal_name).find('#modal-form-cancel').hide();
|
||||
}
|
||||
|
||||
if (options.preventSubmit || options.hideSubmitButton) {
|
||||
$(modal_name).find('#modal-form-submit').hide();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Automatically remove the modal when it is deleted!
|
||||
@ -102,8 +115,11 @@ function createNewModal(options={}) {
|
||||
$(modal_name).on('keydown', 'input', function(event) {
|
||||
if (event.keyCode == 13) {
|
||||
event.preventDefault();
|
||||
// Simulate a click on the 'Submit' button
|
||||
$(modal_name).find('#modal-form-submit').click();
|
||||
|
||||
if (!options.preventSubmit) {
|
||||
// Simulate a click on the 'Submit' button
|
||||
$(modal_name).find('#modal-form-submit').click();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -117,18 +133,7 @@ function createNewModal(options={}) {
|
||||
// Set labels based on supplied options
|
||||
modalSetTitle(modal_name, options.title || '{% trans "Form Title" %}');
|
||||
modalSetSubmitText(modal_name, options.submitText || '{% trans "Submit" %}');
|
||||
modalSetCloseText(modal_name, options.cancelText || '{% trans "Cancel" %}');
|
||||
|
||||
if (options.hideSubmitButton) {
|
||||
$(modal_name).find('#modal-form-submit').hide();
|
||||
}
|
||||
|
||||
if (options.hideCloseButton) {
|
||||
$(modal_name).find('#modal-form-cancel').hide();
|
||||
}
|
||||
|
||||
// Steal keyboard focus
|
||||
$(modal_name).focus();
|
||||
modalSetCloseText(modal_name, options.closeText || '{% trans "Cancel" %}');
|
||||
|
||||
// Return the "name" of the modal
|
||||
return modal_name;
|
||||
@ -274,7 +279,7 @@ function reloadFieldOptions(fieldName, options) {
|
||||
setFieldOptions(fieldName, opts);
|
||||
},
|
||||
error: function() {
|
||||
console.log('Error GETting field options');
|
||||
console.error('Error GETting field options');
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -581,7 +586,7 @@ function showAlertDialog(title, content, options={}) {
|
||||
|
||||
var modal = createNewModal({
|
||||
title: title,
|
||||
cancelText: '{% trans "Close" %}',
|
||||
closeText: '{% trans "Close" %}',
|
||||
hideSubmitButton: true,
|
||||
});
|
||||
|
||||
@ -607,7 +612,7 @@ function showQuestionDialog(title, content, options={}) {
|
||||
var modal = createNewModal({
|
||||
title: title,
|
||||
submitText: options.accept_text || '{% trans "Accept" %}',
|
||||
cancelText: options.cancel_text || '{% trans "Cancel" %}',
|
||||
closeText: options.cancel_text || '{% trans "Cancel" %}',
|
||||
});
|
||||
|
||||
modalSetContent(modal, content);
|
||||
@ -842,7 +847,7 @@ function attachFieldCallback(modal, callback) {
|
||||
// Run the callback function with the new value of the field!
|
||||
callback.action(field.val(), field);
|
||||
} else {
|
||||
console.log(`Value changed for field ${callback.field} - ${field.val()}`);
|
||||
console.info(`Value changed for field ${callback.field} - ${field.val()} (no callback attached)`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1085,8 +1090,8 @@ function launchModalForm(url, options = {}) {
|
||||
showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr));
|
||||
}
|
||||
|
||||
console.log('Modal form error: ' + xhr.status);
|
||||
console.log('Message: ' + xhr.responseText);
|
||||
console.error('Modal form error: ' + xhr.status);
|
||||
console.info('Message: ' + xhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
||||
// Should the ID be rendered for this string
|
||||
function renderId(title, pk, parameters={}) {
|
||||
|
||||
// Default = do not display
|
||||
// Default = do not render
|
||||
var render = false;
|
||||
|
||||
if ('render_pk' in parameters) {
|
||||
@ -297,7 +297,12 @@ function renderSalesOrderShipment(name, data, parameters={}, options={}) {
|
||||
|
||||
var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
|
||||
|
||||
var html = `<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>`;
|
||||
var html = `
|
||||
<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>
|
||||
<span class='float-right'>
|
||||
<small>{% trans "Shipment ID" %}: ${data.pk}</small>
|
||||
</span>
|
||||
`;
|
||||
|
||||
html += renderId('{% trans "Shipment ID" %}', data.pk, parameters);
|
||||
|
||||
@ -384,10 +389,18 @@ function renderSupplierPart(name, data, parameters={}, options={}) {
|
||||
var html = '';
|
||||
|
||||
html += select2Thumbnail(supplier_image);
|
||||
html += select2Thumbnail(part_image);
|
||||
|
||||
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
|
||||
html += ` - <i>${data.part_detail.full_name}</i>`;
|
||||
if (data.part_detail) {
|
||||
html += select2Thumbnail(part_image);
|
||||
}
|
||||
|
||||
if (data.supplier_detail) {
|
||||
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
|
||||
}
|
||||
|
||||
if (data.part_detail) {
|
||||
html += ` - <i>${data.part_detail.full_name}</i>`;
|
||||
}
|
||||
|
||||
html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters);
|
||||
|
||||
|
@ -35,6 +35,7 @@
|
||||
loadSalesOrderTable,
|
||||
newPurchaseOrderFromOrderWizard,
|
||||
newSupplierPartFromOrderWizard,
|
||||
orderParts,
|
||||
removeOrderRowFromOrderWizard,
|
||||
removePurchaseOrderLineItem,
|
||||
loadOrderTotal,
|
||||
@ -259,8 +260,8 @@ function createPurchaseOrder(options={}) {
|
||||
}
|
||||
}
|
||||
},
|
||||
supplier_reference: {},
|
||||
description: {},
|
||||
supplier_reference: {},
|
||||
target_date: {
|
||||
icon: 'fa-calendar-alt',
|
||||
},
|
||||
@ -476,6 +477,280 @@ function exportOrder(redirect_url, options={}) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Create a new form to order parts based on the list of provided parts.
|
||||
*/
|
||||
function orderParts(parts_list, options={}) {
|
||||
|
||||
var parts = [];
|
||||
|
||||
parts_list.forEach(function(part) {
|
||||
if (part.purchaseable) {
|
||||
parts.push(part);
|
||||
}
|
||||
});
|
||||
|
||||
if (parts.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Parts" %}',
|
||||
'{% trans "At least one purchaseable part must be selected" %}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render a single part within the dialog
|
||||
function renderPart(part, opts={}) {
|
||||
|
||||
var pk = part.pk;
|
||||
|
||||
var thumb = thumbnailImage(part.thumbnail || part.image);
|
||||
|
||||
// The "quantity" field should have been provided for each part
|
||||
var quantity = part.quantity || 1;
|
||||
|
||||
if (quantity < 0) {
|
||||
quantity = 0;
|
||||
}
|
||||
|
||||
var quantity_input = constructField(
|
||||
`quantity_${pk}`,
|
||||
{
|
||||
type: 'decimal',
|
||||
min_value: 0,
|
||||
value: quantity,
|
||||
title: '{% trans "Quantity to order" %}',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
var supplier_part_prefix = `
|
||||
<button type='button' class='input-group-text button-row-new-sp' pk='${pk}' title='{% trans "New supplier part" %}'>
|
||||
<span class='fas fa-plus-circle icon-green'></span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
var supplier_part_input = constructField(
|
||||
`part_${pk}`,
|
||||
{
|
||||
type: 'related field',
|
||||
required: true,
|
||||
prefixRaw: supplier_part_prefix,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
var purchase_order_prefix = `
|
||||
<button type='button' class='input-group-text button-row-new-po' pk='${pk}' title='{% trans "New purchase order" %}'>
|
||||
<span class='fas fa-plus-circle icon-green'></span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
var purchase_order_input = constructField(
|
||||
`order_${pk}`,
|
||||
{
|
||||
type: 'related field',
|
||||
required: true,
|
||||
prefixRaw: purchase_order_prefix,
|
||||
},
|
||||
{
|
||||
hideLabels: 'true',
|
||||
}
|
||||
);
|
||||
|
||||
var buttons = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
if (parts.length > 1) {
|
||||
buttons += makeIconButton(
|
||||
'fa-times icon-red',
|
||||
'button-row-remove',
|
||||
pk,
|
||||
'{% trans "Remove row" %}',
|
||||
);
|
||||
}
|
||||
|
||||
// Button to add row to purchase order
|
||||
buttons += makeIconButton(
|
||||
'fa-shopping-cart icon-blue',
|
||||
'button-row-add',
|
||||
pk,
|
||||
'{% trans "Add to purchase order" %}',
|
||||
);
|
||||
|
||||
buttons += `</div>`;
|
||||
|
||||
var html = `
|
||||
<tr id='order_row_${pk}' class='part-order-row'>
|
||||
<td id='td_part_${pk}'>${thumb} ${part.full_name}</td>
|
||||
<td id='td_supplier_part_${pk}'>${supplier_part_input}</td>
|
||||
<td id='td_order_${pk}'>${purchase_order_input}</td>
|
||||
<td id='td_quantity_${pk}'>${quantity_input}</td>
|
||||
<td id='td_actions_${pk}'>${buttons}</td>
|
||||
</tr>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
var table_entries = '';
|
||||
|
||||
parts.forEach(function(part) {
|
||||
table_entries += renderPart(part);
|
||||
});
|
||||
|
||||
var html = '';
|
||||
|
||||
// Add table
|
||||
html += `
|
||||
<table class='table table-striped table-condensed' id='order-parts-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Part" %}</th>
|
||||
<th style='min-width: 300px;'>{% trans "Supplier Part" %}</th>
|
||||
<th style='min-width: 300px;'>{% trans "Purchase Order" %}</th>
|
||||
<th style='min-width: 50px;'>{% trans "Quantity" %}</th>
|
||||
<th><!-- Actions --></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${table_entries}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
constructFormBody({}, {
|
||||
preFormContent: html,
|
||||
title: '{% trans "Order Parts" %}',
|
||||
preventSubmit: true,
|
||||
closeText: '{% trans "Close" %}',
|
||||
afterRender: function(fields, opts) {
|
||||
// TODO
|
||||
parts.forEach(function(part) {
|
||||
// Configure the "supplier part" field
|
||||
initializeRelatedField({
|
||||
name: `part_${part.pk}`,
|
||||
model: 'supplierpart',
|
||||
api_url: '{% url "api-supplier-part-list" %}',
|
||||
required: true,
|
||||
type: 'related field',
|
||||
auto_fill: true,
|
||||
filters: {
|
||||
part: part.pk,
|
||||
supplier_detail: true,
|
||||
part_detail: false,
|
||||
},
|
||||
noResults: function(query) {
|
||||
return '{% trans "No matching supplier parts" %}';
|
||||
}
|
||||
}, null, opts);
|
||||
|
||||
// Configure the "purchase order" field
|
||||
initializeRelatedField({
|
||||
name: `order_${part.pk}`,
|
||||
model: 'purchaseorder',
|
||||
api_url: '{% url "api-po-list" %}',
|
||||
required: true,
|
||||
type: 'related field',
|
||||
auto_fill: false,
|
||||
filters: {
|
||||
status: {{ PurchaseOrderStatus.PENDING }},
|
||||
supplier_detail: true,
|
||||
},
|
||||
noResults: function(query) {
|
||||
return '{% trans "No matching purchase orders" %}';
|
||||
}
|
||||
}, null, opts);
|
||||
});
|
||||
|
||||
// Add callback for "add to purchase order" button
|
||||
$(opts.modal).find('.button-row-add').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
opts.field_suffix = null;
|
||||
|
||||
// Extract information from the row
|
||||
var data = {
|
||||
quantity: getFormFieldValue(`quantity_${pk}`, {type: 'decimal'}, opts),
|
||||
part: getFormFieldValue(`part_${pk}`, {}, opts),
|
||||
order: getFormFieldValue(`order_${pk}`, {}, opts),
|
||||
};
|
||||
|
||||
// Duplicate the form options, to prevent 'field_suffix' override
|
||||
var row_opts = Object.assign(opts);
|
||||
row_opts.field_suffix = `_${pk}`;
|
||||
|
||||
inventreePut(
|
||||
'{% url "api-po-line-list" %}',
|
||||
data,
|
||||
{
|
||||
method: 'POST',
|
||||
success: function(response) {
|
||||
// Remove the row
|
||||
$(opts.modal).find(`#order_row_${pk}`).remove();
|
||||
},
|
||||
error: function(xhr) {
|
||||
switch (xhr.status) {
|
||||
case 400:
|
||||
handleFormErrors(xhr.responseJSON, fields, row_opts);
|
||||
break;
|
||||
default:
|
||||
console.error(`Error adding line to purchase order`);
|
||||
showApiError(xhr, options.url);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Add callback for "remove row" button
|
||||
$(opts.modal).find('.button-row-remove').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
$(opts.modal).find(`#order_row_${pk}`).remove();
|
||||
});
|
||||
|
||||
// Add callback for "new supplier part" button
|
||||
$(opts.modal).find('.button-row-new-sp').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// Launch dialog to create new supplier part
|
||||
createSupplierPart({
|
||||
part: pk,
|
||||
onSuccess: function(response) {
|
||||
setRelatedFieldData(
|
||||
`part_${pk}`,
|
||||
response,
|
||||
opts
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add callback for "new purchase order" button
|
||||
$(opts.modal).find('.button-row-new-po').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// Launch dialog to create new purchase order
|
||||
createPurchaseOrder({
|
||||
onSuccess: function(response) {
|
||||
setRelatedFieldData(
|
||||
`order_${pk}`,
|
||||
response,
|
||||
opts
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function newPurchaseOrderFromOrderWizard(e) {
|
||||
/* Create a new purchase order directly from an order form.
|
||||
* Launches a secondary modal and (if successful),
|
||||
@ -681,12 +956,14 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
);
|
||||
}
|
||||
|
||||
buttons += makeIconButton(
|
||||
'fa-times icon-red',
|
||||
'button-row-remove',
|
||||
pk,
|
||||
'{% trans "Remove row" %}',
|
||||
);
|
||||
if (line_items.length > 1) {
|
||||
buttons += makeIconButton(
|
||||
'fa-times icon-red',
|
||||
'button-row-remove',
|
||||
pk,
|
||||
'{% trans "Remove row" %}',
|
||||
);
|
||||
}
|
||||
|
||||
buttons += '</div>';
|
||||
|
||||
@ -1155,7 +1432,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
if (!line_item) {
|
||||
console.log('WARNING: getRowByUniqueId returned null');
|
||||
console.warn('getRowByUniqueId returned null');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1414,12 +1691,12 @@ function loadPurchaseOrderExtraLineTable(table, options={}) {
|
||||
options.params = options.params || {};
|
||||
|
||||
if (!options.order) {
|
||||
console.log('ERROR: function called without order ID');
|
||||
console.error('function called without order ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.log('ERROR: function called without order status');
|
||||
console.error('function called without order status');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2541,12 +2818,12 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
||||
options.params = options.params || {};
|
||||
|
||||
if (!options.order) {
|
||||
console.log('ERROR: function called without order ID');
|
||||
console.error('function called without order ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.log('ERROR: function called without order status');
|
||||
console.error('function called without order status');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3049,12 +3326,12 @@ function loadSalesOrderExtraLineTable(table, options={}) {
|
||||
options.params = options.params || {};
|
||||
|
||||
if (!options.order) {
|
||||
console.log('ERROR: function called without order ID');
|
||||
console.error('function called without order ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.log('ERROR: function called without order status');
|
||||
console.error('function called without order status');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -876,7 +876,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
|
||||
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
if (!line_item) {
|
||||
console.log('WARNING: getRowByUniqueId returned null');
|
||||
console.warn('getRowByUniqueId returned null');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1564,15 +1564,16 @@ function loadPartTable(table, url, options={}) {
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.pk);
|
||||
selections.forEach(function(part) {
|
||||
parts.push(part);
|
||||
});
|
||||
|
||||
launchModalForm('/order/purchase-order/order-parts/', {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
});
|
||||
orderParts(
|
||||
parts,
|
||||
{
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#multi-part-category').click(function() {
|
||||
@ -1603,19 +1604,6 @@ function loadPartTable(table, url, options={}) {
|
||||
|
||||
printPartLabels(items);
|
||||
});
|
||||
|
||||
$('#multi-part-export').click(function() {
|
||||
var selections = $(table).bootstrapTable('getSelections');
|
||||
|
||||
var parts = '';
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts += item.pk;
|
||||
parts += ',';
|
||||
});
|
||||
|
||||
location.href = '/part/export/?parts=' + parts;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -39,7 +39,7 @@ function downloadTableData(table, opts={}) {
|
||||
var url = table_options.url;
|
||||
|
||||
if (!url) {
|
||||
console.log('Error: downloadTableData could not find "url" parameter.');
|
||||
console.error('downloadTableData could not find "url" parameter.');
|
||||
}
|
||||
|
||||
var query_params = table_options.query_params || {};
|
||||
@ -343,7 +343,7 @@ $.fn.inventreeTable = function(options) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(`Could not get list of visible columns for table '${tableName}'`);
|
||||
console.error(`Could not get list of visible columns for table '${tableName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user