Merge pull request #2770 from SchrodingersGat/order-parts-wizard

Order parts wizard
This commit is contained in:
Oliver 2022-05-03 14:56:53 +10:00 committed by GitHub
commit e938870b32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 473 additions and 122 deletions

View File

@ -312,7 +312,7 @@ class SupplierPartList(generics.ListCreateAPIView):
try: try:
params = self.request.query_params params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', None)) 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['manufacturer_detail'] = str2bool(params.get('manufacturer_detail', None))
kwargs['pretty'] = str2bool(params.get('pretty', None)) kwargs['pretty'] = str2bool(params.get('pretty', None))
except AttributeError: except AttributeError:

View File

@ -223,14 +223,30 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
if order_detail is not True: if order_detail is not True:
self.fields.pop('order_detail') self.fields.pop('order_detail')
quantity = serializers.FloatField(default=1) quantity = serializers.FloatField(min_value=0, required=True)
received = serializers.FloatField(default=0)
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) overdue = serializers.BooleanField(required=False, read_only=True)
total_price = serializers.FloatField(read_only=True) total_price = serializers.FloatField(read_only=True)
part_detail = PartBriefSerializer(source='get_base_part', many=False, 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) supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
purchase_price = InvenTreeMoneySerializer( purchase_price = InvenTreeMoneySerializer(
@ -248,6 +264,32 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False) 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: class Meta:
model = order.models.PurchaseOrderLineItem model = order.models.PurchaseOrderLineItem

View File

@ -169,13 +169,18 @@
</button> </button>
<ul class='dropdown-menu'> <ul class='dropdown-menu'>
{% if roles.part.change %} {% 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 %} {% 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 %} {% 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 %} {% endif %}
<li><a class='dropdown-item' href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
</ul> </ul>
</div> </div>
{% include "filter_list.html" with id="parts" %} {% include "filter_list.html" with id="parts" %}

View File

@ -536,6 +536,22 @@
{% endif %} {% endif %}
$("#part-order").click(function() { $("#part-order").click(function() {
inventreeGet(
'{% url "api-part-detail" part.pk %}',
{},
{
success: function(part) {
orderParts(
[part],
{}
);
}
}
);
return;
launchModalForm("{% url 'order-parts' %}", { launchModalForm("{% url 'order-parts' %}", {
data: { data: {
part: {{ part.id }}, part: {{ part.id }},

View File

@ -105,7 +105,7 @@ function inventreeFormDataUpload(url, data, options={}) {
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.log('Form data upload failure: ' + status); console.error('Form data upload failure: ' + status);
if (options.error) { if (options.error) {
options.error(xhr, status, error); options.error(xhr, status, error);

View File

@ -86,7 +86,6 @@ function onCameraAvailable(hasCamera, options) {
function onBarcodeScanCompleted(result, options) { function onBarcodeScanCompleted(result, options) {
if (result.data == '') return; if (result.data == '') return;
console.log('decoded qr code:', result.data);
stopQrScanner(); stopQrScanner();
postBarcodeData(result.data, options); postBarcodeData(result.data, options);
} }

View File

@ -129,7 +129,7 @@ function constructBomUploadTable(data, options={}) {
var modal = createNewModal({ var modal = createNewModal({
title: '{% trans "Row Data" %}', title: '{% trans "Row Data" %}',
cancelText: '{% trans "Close" %}', closeText: '{% trans "Close" %}',
hideSubmitButton: true hideSubmitButton: true
}); });
@ -617,7 +617,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
}, },
}, },
preFormContent: html, preFormContent: html,
cancelText: '{% trans "Close" %}', closeText: '{% trans "Close" %}',
submitText: '{% trans "Add Substitute" %}', submitText: '{% trans "Add Substitute" %}',
title: '{% trans "Edit BOM Item Substitutes" %}', title: '{% trans "Edit BOM Item Substitutes" %}',
afterRender: function(fields, opts) { afterRender: function(fields, opts) {
@ -1061,7 +1061,7 @@ function loadBomTable(table, options={}) {
table.bootstrapTable('append', response); table.bootstrapTable('append', response);
}, },
error: function(xhr) { error: function(xhr) {
console.log('Error requesting BOM for part=' + part_pk); console.error('Error requesting BOM for part=' + part_pk);
showApiError(xhr); showApiError(xhr);
} }
} }

View File

@ -115,10 +115,6 @@ function supplierPartFields() {
return { return {
part: {}, part: {},
supplier: {},
SKU: {
icon: 'fa-hashtag',
},
manufacturer_part: { manufacturer_part: {
filters: { filters: {
part_detail: true, part_detail: true,
@ -126,6 +122,10 @@ function supplierPartFields() {
}, },
auto_fill: true, auto_fill: true,
}, },
supplier: {},
SKU: {
icon: 'fa-hashtag',
},
description: {}, description: {},
link: { link: {
icon: 'fa-link', icon: 'fa-link',

View File

@ -62,7 +62,7 @@ function loadTableFilters(tableKey) {
if (f.length == 2) { if (f.length == 2) {
filters[f[0]] = f[1]; filters[f[0]] = f[1];
} else { } 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); var element = $(target);
if (!element || !element.exists()) { if (!element || !element.exists()) {
console.log(`WARNING: setupFilterList could not find target '${target}'`); console.warn(`setupFilterList could not find target '${target}'`);
return; return;
} }

View File

@ -135,7 +135,7 @@ function getApiEndpointOptions(url, callback) {
success: callback, success: callback,
error: function(xhr) { error: function(xhr) {
// TODO: Handle error // TODO: Handle error
console.log(`ERROR in getApiEndpointOptions at '${url}'`); console.error(`Error in getApiEndpointOptions at '${url}'`);
showApiError(xhr, url); showApiError(xhr, url);
} }
}); });
@ -227,7 +227,7 @@ function constructChangeForm(fields, options) {
}, },
error: function(xhr) { error: function(xhr) {
// TODO: Handle error here // TODO: Handle error here
console.log(`ERROR in constructChangeForm at '${options.url}'`); console.error(`Error in constructChangeForm at '${options.url}'`);
showApiError(xhr, options.url); showApiError(xhr, options.url);
} }
@ -268,7 +268,7 @@ function constructDeleteForm(fields, options) {
}, },
error: function(xhr) { error: function(xhr) {
// TODO: Handle error here // TODO: Handle error here
console.log(`ERROR in constructDeleteForm at '${options.url}`); console.error(`Error in constructDeleteForm at '${options.url}`);
showApiError(xhr, options.url); showApiError(xhr, options.url);
} }
@ -354,7 +354,7 @@ function constructForm(url, options) {
icon: 'fas fa-user-times', icon: 'fas fa-user-times',
}); });
console.log(`'POST action unavailable at ${url}`); console.warn(`'POST action unavailable at ${url}`);
} }
break; break;
case 'PUT': case 'PUT':
@ -369,7 +369,7 @@ function constructForm(url, options) {
icon: 'fas fa-user-times', icon: 'fas fa-user-times',
}); });
console.log(`${options.method} action unavailable at ${url}`); console.warn(`${options.method} action unavailable at ${url}`);
} }
break; break;
case 'DELETE': case 'DELETE':
@ -383,7 +383,7 @@ function constructForm(url, options) {
icon: 'fas fa-user-times', icon: 'fas fa-user-times',
}); });
console.log(`DELETE action unavailable at ${url}`); console.warn(`DELETE action unavailable at ${url}`);
} }
break; break;
case 'GET': case 'GET':
@ -397,11 +397,11 @@ function constructForm(url, options) {
icon: 'fas fa-user-times', icon: 'fas fa-user-times',
}); });
console.log(`GET action unavailable at ${url}`); console.warn(`GET action unavailable at ${url}`);
} }
break; break;
default: default:
console.log(`constructForm() called with invalid method '${options.method}'`); console.warn(`constructForm() called with invalid method '${options.method}'`);
break; break;
} }
}); });
@ -731,7 +731,7 @@ function submitFormData(fields, options) {
data[name] = value; data[name] = value;
} }
} else { } 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: default:
$(options.modal).modal('hide'); $(options.modal).modal('hide');
console.log(`upload error at ${options.url}`); console.error(`Upload error at ${options.url}`);
showApiError(xhr, options.url); showApiError(xhr, options.url);
break; break;
} }
@ -827,7 +827,7 @@ function updateFieldValue(name, value, field, options) {
var el = getFormFieldElement(name, options); var el = getFormFieldElement(name, options);
if (!el) { if (!el) {
console.log(`WARNING: updateFieldValue could not find field '${name}'`); console.warn(`updateFieldValue could not find field '${name}'`);
return; return;
} }
@ -870,7 +870,7 @@ function getFormFieldElement(name, options) {
} }
if (!el.exists) { 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; return el;
@ -918,7 +918,7 @@ function getFormFieldValue(name, field={}, options={}) {
var el = getFormFieldElement(name, options); var el = getFormFieldElement(name, options);
if (!el.exists()) { if (!el.exists()) {
console.log(`ERROR: getFormFieldValue could not locate field '${name}'`); console.error(`getFormFieldValue could not locate field '${name}'`);
return null; return null;
} }
@ -1104,7 +1104,7 @@ function handleNestedErrors(errors, field_name, options={}) {
// Nest list must be provided! // Nest list must be provided!
if (!nest_list) { if (!nest_list) {
console.log(`WARNING: handleNestedErrors missing nesting options for field '${fieldName}'`); console.warn(`handleNestedErrors missing nesting options for field '${fieldName}'`);
return; return;
} }
@ -1113,7 +1113,7 @@ function handleNestedErrors(errors, field_name, options={}) {
var error_item = error_list[idx]; var error_item = error_list[idx];
if (idx >= nest_list.length) { 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; break;
} }
@ -1218,29 +1218,26 @@ function handleFormErrors(errors, fields={}, options={}) {
for (var field_name in errors) { 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)) { var field_errors = errors[field_name];
// This is a "nested" field
handleNestedErrors(errors, field_name, options);
} else {
// This is a "simple" field
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)) { // Add an entry for each returned error message
first_error_field = field_name; for (var ii = field_errors.length-1; ii >= 0; ii--) {
}
// Add an entry for each returned error message var error_text = field_errors[ii];
for (var ii = field_errors.length-1; ii >= 0; 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); field_dom.append(error_html);
} else { } 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) { if (!el) {
console.log(`WARNING: addClearCallback could not find field '${name}'`); console.warn(`addClearCallback could not find field '${name}'`);
return; return;
} }
@ -1582,7 +1579,7 @@ function initializeRelatedField(field, fields, options={}) {
var name = field.name; var name = field.name;
if (!field.api_url) { if (!field.api_url) {
console.log(`WARNING: Related field '${name}' missing 'api_url' parameter.`); console.warn(`Related field '${name}' missing 'api_url' parameter.`);
return; return;
} }
@ -1712,7 +1709,7 @@ function initializeRelatedField(field, fields, options={}) {
return $(html); return $(html);
} else { } else {
// Return a simple renderering // 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}`; return `${name} - ${item.id}`;
} }
}, },
@ -1742,7 +1739,7 @@ function initializeRelatedField(field, fields, options={}) {
return $(html); return $(html);
} else { } else {
// Return a simple renderering // 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}`; return `${name} - ${item.id}`;
} }
} }
@ -1780,6 +1777,11 @@ function initializeRelatedField(field, fields, options={}) {
// Only a single result is available, given the provided filters // Only a single result is available, given the provided filters
if (data.count == 1) { if (data.count == 1) {
setRelatedFieldData(name, data.results[0], options); 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) { if (html != null) {
return html; return html;
} else { } else {
console.log(`ERROR: Rendering not implemented for model '${model}'`); console.error(`Rendering not implemented for model '${model}'`);
// Simple text rendering // Simple text rendering
return `${model} - ID ${data.id}`; return `${model} - ID ${data.id}`;
} }
@ -1924,6 +1926,10 @@ function renderModelData(name, model, data, parameters, options) {
function getFieldName(name, options={}) { function getFieldName(name, options={}) {
var field_name = name; var field_name = name;
if (options.field_suffix) {
field_name += options.field_suffix;
}
if (options && options.depth) { if (options && options.depth) {
field_name += `_${options.depth}`; field_name += `_${options.depth}`;
} }
@ -2196,7 +2202,7 @@ function constructInput(name, parameters, options={}) {
if (func != null) { if (func != null) {
html = func(name, parameters, options); html = func(name, parameters, options);
} else { } else {
console.log(`WARNING: Unhandled form field type: '${parameters.type}'`); console.warn(`Unhandled form field type: '${parameters.type}'`);
} }
return html; return html;
@ -2499,12 +2505,12 @@ function constructHelpText(name, parameters) {
function selectImportFields(url, data={}, options={}) { function selectImportFields(url, data={}, options={}) {
if (!data.model_fields) { if (!data.model_fields) {
console.log(`WARNING: selectImportFields is missing 'model_fields'`); console.warn(`selectImportFields is missing 'model_fields'`);
return; return;
} }
if (!data.file_fields) { if (!data.file_fields) {
console.log(`WARNING: selectImportFields is missing 'file_fields'`); console.warn(`selectImportFields is missing 'file_fields'`);
return; return;
} }
@ -2595,7 +2601,7 @@ function selectImportFields(url, data={}, options={}) {
default: default:
$(opts.modal).modal('hide'); $(opts.modal).modal('hide');
console.log(`upload error at ${opts.url}`); console.error(`upload error at ${opts.url}`);
showApiError(xhr, opts.url); showApiError(xhr, opts.url);
break; break;
} }

View File

@ -85,12 +85,25 @@ function createNewModal(options={}) {
var modal_name = `#modal-form-${id}`; var modal_name = `#modal-form-${id}`;
// Callback *after* the modal has been rendered
$(modal_name).on('shown.bs.modal', function() { $(modal_name).on('shown.bs.modal', function() {
$(modal_name + ' .modal-form-content').scrollTop(0); $(modal_name + ' .modal-form-content').scrollTop(0);
if (options.focus) { if (options.focus) {
getFieldByName(modal_name, options.focus).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! // Automatically remove the modal when it is deleted!
@ -102,8 +115,11 @@ function createNewModal(options={}) {
$(modal_name).on('keydown', 'input', function(event) { $(modal_name).on('keydown', 'input', function(event) {
if (event.keyCode == 13) { if (event.keyCode == 13) {
event.preventDefault(); 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; return false;
} }
@ -117,18 +133,7 @@ function createNewModal(options={}) {
// Set labels based on supplied options // Set labels based on supplied options
modalSetTitle(modal_name, options.title || '{% trans "Form Title" %}'); modalSetTitle(modal_name, options.title || '{% trans "Form Title" %}');
modalSetSubmitText(modal_name, options.submitText || '{% trans "Submit" %}'); modalSetSubmitText(modal_name, options.submitText || '{% trans "Submit" %}');
modalSetCloseText(modal_name, options.cancelText || '{% trans "Cancel" %}'); modalSetCloseText(modal_name, options.closeText || '{% 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();
// Return the "name" of the modal // Return the "name" of the modal
return modal_name; return modal_name;
@ -274,7 +279,7 @@ function reloadFieldOptions(fieldName, options) {
setFieldOptions(fieldName, opts); setFieldOptions(fieldName, opts);
}, },
error: function() { 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({ var modal = createNewModal({
title: title, title: title,
cancelText: '{% trans "Close" %}', closeText: '{% trans "Close" %}',
hideSubmitButton: true, hideSubmitButton: true,
}); });
@ -607,7 +612,7 @@ function showQuestionDialog(title, content, options={}) {
var modal = createNewModal({ var modal = createNewModal({
title: title, title: title,
submitText: options.accept_text || '{% trans "Accept" %}', submitText: options.accept_text || '{% trans "Accept" %}',
cancelText: options.cancel_text || '{% trans "Cancel" %}', closeText: options.cancel_text || '{% trans "Cancel" %}',
}); });
modalSetContent(modal, content); modalSetContent(modal, content);
@ -842,7 +847,7 @@ function attachFieldCallback(modal, callback) {
// Run the callback function with the new value of the field! // Run the callback function with the new value of the field!
callback.action(field.val(), field); callback.action(field.val(), field);
} else { } 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)); showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr));
} }
console.log('Modal form error: ' + xhr.status); console.error('Modal form error: ' + xhr.status);
console.log('Message: ' + xhr.responseText); console.info('Message: ' + xhr.responseText);
} }
}; };

View File

@ -34,7 +34,7 @@
// Should the ID be rendered for this string // Should the ID be rendered for this string
function renderId(title, pk, parameters={}) { function renderId(title, pk, parameters={}) {
// Default = do not display // Default = do not render
var render = false; var render = false;
if ('render_pk' in parameters) { if ('render_pk' in parameters) {
@ -297,7 +297,12 @@ function renderSalesOrderShipment(name, data, parameters={}, options={}) {
var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX; 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); html += renderId('{% trans "Shipment ID" %}', data.pk, parameters);
@ -384,10 +389,18 @@ function renderSupplierPart(name, data, parameters={}, options={}) {
var html = ''; var html = '';
html += select2Thumbnail(supplier_image); html += select2Thumbnail(supplier_image);
html += select2Thumbnail(part_image);
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`; if (data.part_detail) {
html += ` - <i>${data.part_detail.full_name}</i>`; 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); html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters);

View File

@ -35,6 +35,7 @@
loadSalesOrderTable, loadSalesOrderTable,
newPurchaseOrderFromOrderWizard, newPurchaseOrderFromOrderWizard,
newSupplierPartFromOrderWizard, newSupplierPartFromOrderWizard,
orderParts,
removeOrderRowFromOrderWizard, removeOrderRowFromOrderWizard,
removePurchaseOrderLineItem, removePurchaseOrderLineItem,
loadOrderTotal, loadOrderTotal,
@ -259,8 +260,8 @@ function createPurchaseOrder(options={}) {
} }
} }
}, },
supplier_reference: {},
description: {}, description: {},
supplier_reference: {},
target_date: { target_date: {
icon: 'fa-calendar-alt', 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) { function newPurchaseOrderFromOrderWizard(e) {
/* Create a new purchase order directly from an order form. /* Create a new purchase order directly from an order form.
* Launches a secondary modal and (if successful), * Launches a secondary modal and (if successful),
@ -681,12 +956,14 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
); );
} }
buttons += makeIconButton( if (line_items.length > 1) {
'fa-times icon-red', buttons += makeIconButton(
'button-row-remove', 'fa-times icon-red',
pk, 'button-row-remove',
'{% trans "Remove row" %}', pk,
); '{% trans "Remove row" %}',
);
}
buttons += '</div>'; buttons += '</div>';
@ -1155,7 +1432,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
if (!line_item) { if (!line_item) {
console.log('WARNING: getRowByUniqueId returned null'); console.warn('getRowByUniqueId returned null');
return; return;
} }
@ -1414,12 +1691,12 @@ function loadPurchaseOrderExtraLineTable(table, options={}) {
options.params = options.params || {}; options.params = options.params || {};
if (!options.order) { if (!options.order) {
console.log('ERROR: function called without order ID'); console.error('function called without order ID');
return; return;
} }
if (!options.status) { if (!options.status) {
console.log('ERROR: function called without order status'); console.error('function called without order status');
return; return;
} }
@ -2541,12 +2818,12 @@ function loadSalesOrderLineItemTable(table, options={}) {
options.params = options.params || {}; options.params = options.params || {};
if (!options.order) { if (!options.order) {
console.log('ERROR: function called without order ID'); console.error('function called without order ID');
return; return;
} }
if (!options.status) { if (!options.status) {
console.log('ERROR: function called without order status'); console.error('function called without order status');
return; return;
} }
@ -3049,12 +3326,12 @@ function loadSalesOrderExtraLineTable(table, options={}) {
options.params = options.params || {}; options.params = options.params || {};
if (!options.order) { if (!options.order) {
console.log('ERROR: function called without order ID'); console.error('function called without order ID');
return; return;
} }
if (!options.status) { if (!options.status) {
console.log('ERROR: function called without order status'); console.error('function called without order status');
return; return;
} }

View File

@ -876,7 +876,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
if (!line_item) { if (!line_item) {
console.log('WARNING: getRowByUniqueId returned null'); console.warn('getRowByUniqueId returned null');
return; return;
} }
@ -1564,15 +1564,16 @@ function loadPartTable(table, url, options={}) {
var parts = []; var parts = [];
selections.forEach(function(item) { selections.forEach(function(part) {
parts.push(item.pk); parts.push(part);
}); });
launchModalForm('/order/purchase-order/order-parts/', { orderParts(
data: { parts,
parts: parts, {
},
}); }
);
}); });
$('#multi-part-category').click(function() { $('#multi-part-category').click(function() {
@ -1603,19 +1604,6 @@ function loadPartTable(table, url, options={}) {
printPartLabels(items); 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;
});
} }

View File

@ -39,7 +39,7 @@ function downloadTableData(table, opts={}) {
var url = table_options.url; var url = table_options.url;
if (!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 || {}; var query_params = table_options.query_params || {};
@ -343,7 +343,7 @@ $.fn.inventreeTable = function(options) {
} }
}); });
} else { } 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}'`);
} }
} }