InvenTree/InvenTree/templates/js/translated/purchase_order.js
github-actions[bot] 698798fee7
Order table improvements (#5151) (#5152)
- prevent "double loading" of order tables

(cherry picked from commit 17c2070503)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-04 16:23:43 +10:00

2210 lines
69 KiB
JavaScript

{% load i18n %}
{% load inventree_extras %}
/* globals
addClearCallback,
addFieldCallback,
barcodeDialog,
calculateTotalPrice,
clearEvents,
closeModal,
constructFormBody,
companyFormFields,
constructField,
constructForm,
constructOrderTableButtons,
createSupplierPart,
endDate,
formatCurrency,
formatDecimal,
FullCalendar,
initializeChoiceField,
initializeRelatedField,
getFormFieldElement,
getFormFieldValue,
getTableData,
global_settings,
handleFormErrors,
handleFormSuccess,
imageHoverIcon,
inventreeGet,
inventreeLoad,
inventreePut,
launchModalForm,
loadTableFilters,
makeCopyButton,
makeDeleteButton,
makeEditButton,
makeIconBadge,
makeIconButton,
makeProgressBar,
makeRemoveButton,
purchaseOrderStatusDisplay,
reloadBootstrapTable,
renderClipboard,
renderDate,
renderLink,
showAlertDialog,
showApiError,
showBarcodeMessage,
setRelatedFieldData,
setupFilterList,
startDate,
stockCodes,
supplierPartFields,
thumbnailImage,
updateFieldValue,
wrapButtons,
*/
/* exported
cancelPurchaseOrder,
completePurchaseOrder,
createPurchaseOrder,
createPurchaseOrderLineItem,
duplicatePurchaseOrder,
editPurchaseOrder,
editPurchaseOrderLineItem,
issuePurchaseOrder,
loadPurchaseOrderLineItemTable,
loadPurchaseOrderTable,
newPurchaseOrderFromOrderWizard,
newSupplierPartFromOrderWizard,
orderParts,
removeOrderRowFromOrderWizard,
removePurchaseOrderLineItem,
*/
/*
* Construct a set of fields for a purchase order form
*/
function purchaseOrderFields(options={}) {
var fields = {
reference: {
icon: 'fa-hashtag',
},
description: {},
supplier: {
icon: 'fa-building',
secondary: {
title: '{% trans "Add Supplier" %}',
fields: function() {
var fields = companyFormFields();
fields.is_supplier.value = true;
return fields;
}
}
},
supplier_reference: {},
project_code: {
icon: 'fa-list',
},
order_currency: {
icon: 'fa-coins',
},
target_date: {
icon: 'fa-calendar-alt',
},
link: {
icon: 'fa-link',
},
contact: {
icon: 'fa-user',
adjustFilters: function(filters) {
let supplier = getFormFieldValue('supplier', {}, {modal: options.modal});
if (supplier) {
filters.company = supplier;
}
return filters;
}
},
address: {
icon: 'fa-map',
adjustFilters: function(filters) {
let supplier = getFormFieldValue('supplier', {}, {modal: options.modal});
if (supplier) {
filters.company = supplier;
}
return filters;
}
},
responsible: {
icon: 'fa-user',
},
};
if (options.supplier) {
fields.supplier.value = options.supplier;
}
if (options.hide_supplier) {
fields.supplier.hidden = true;
}
// Add fields for order duplication (only if required)
if (options.duplicate_order) {
fields.duplicate_order = {
value: options.duplicate_order,
group: 'duplicate',
required: 'true',
type: 'related field',
model: 'purchaseorder',
filters: {
supplier_detail: true,
},
api_url: '{% url "api-po-list" %}',
label: '{% trans "Purchase Order" %}',
help_text: '{% trans "Select purchase order to duplicate" %}',
};
fields.duplicate_line_items = {
value: true,
group: 'duplicate',
type: 'boolean',
label: '{% trans "Duplicate Line Items" %}',
help_text: '{% trans "Duplicate all line items from the selected order" %}',
};
fields.duplicate_extra_lines = {
value: true,
group: 'duplicate',
type: 'boolean',
label: '{% trans "Duplicate Extra Lines" %}',
help_text: '{% trans "Duplicate extra line items from the selected order" %}',
};
}
if (!global_settings.PROJECT_CODES_ENABLED) {
delete fields.project_code;
}
return fields;
}
/*
* Edit an existing PurchaseOrder
*/
function editPurchaseOrder(pk, options={}) {
var fields = purchaseOrderFields(options);
constructForm(`{% url "api-po-list" %}${pk}/`, {
fields: fields,
title: '{% trans "Edit Purchase Order" %}',
onSuccess: function(response) {
handleFormSuccess(response, options);
}
});
}
// Create a new PurchaseOrder
function createPurchaseOrder(options={}) {
var fields = purchaseOrderFields(options);
var groups = {};
if (options.duplicate_order) {
groups.duplicate = {
title: '{% trans "Duplication Options" %}',
collapsible: false,
};
}
constructForm('{% url "api-po-list" %}', {
method: 'POST',
fields: fields,
groups: groups,
data: options.data,
onSuccess: function(data) {
if (options.onSuccess) {
options.onSuccess(data);
} else {
// Default action is to redirect browser to the new PurchaseOrder
location.href = `/order/purchase-order/${data.pk}/`;
}
},
title: options.title || '{% trans "Create Purchase Order" %}',
});
}
/*
* Duplicate an existing PurchaseOrder
* Provides user with option to duplicate line items for the order also.
*/
function duplicatePurchaseOrder(order_id, options={}) {
options.duplicate_order = order_id;
inventreeGet(`{% url "api-po-list" %}${order_id}/`, {}, {
success: function(data) {
// Clear out data we do not want to be duplicated
delete data['pk'];
delete data['reference'];
options.data = data;
createPurchaseOrder(options);
}
});
}
/* Construct a set of fields for the PurchaseOrderLineItem form */
function poLineItemFields(options={}) {
var fields = {
order: {
filters: {
supplier_detail: true,
}
},
part: {
icon: 'fa-shapes',
filters: {
part_detail: true,
supplier_detail: true,
supplier: options.supplier,
},
onEdit: function(value, name, field, opts) {
// If the pack_quantity != 1, add a note to the field
var pack_quantity = 1;
var units = '';
var supplier_part_id = value;
var quantity = getFormFieldValue('quantity', {}, opts);
// Remove any existing note fields
$(opts.modal).find('#info-pack-size').remove();
if (value == null) {
return;
}
// Request information about the particular supplier part
inventreeGet(`{% url "api-supplier-part-list" %}${value}/`,
{
part_detail: true,
},
{
success: function(response) {
// Extract information from the returned query
pack_quantity = response.pack_quantity_native || 1;
units = response.part_detail.units || '';
},
}
).then(function() {
// Update pack size information
if (pack_quantity != 1) {
var txt = `<span class='fas fa-info-circle icon-blue'></span> {% trans "Pack Quantity" %}: ${formatDecimal(pack_quantity)} ${units}`;
$(opts.modal).find('#hint_id_quantity').after(`<div class='form-info-message' id='info-pack-size'>${txt}</div>`);
}
}).then(function() {
// Update pricing data (if available)
if (options.update_pricing) {
inventreeGet(
'{% url "api-part-supplier-price-list" %}',
{
part: supplier_part_id,
ordering: 'quantity',
},
{
success: function(response) {
// Returned prices are in increasing order of quantity
if (response.length > 0) {
let index = 0;
for (var idx = 0; idx < response.length; idx++) {
if (response[idx].quantity > quantity) {
break;
}
index = idx;
}
// Update price and currency data in the form
updateFieldValue('purchase_price', response[index].price, {}, opts);
updateFieldValue('purchase_price_currency', response[index].price_currency, {}, opts);
}
}
}
);
}
});
},
secondary: {
method: 'POST',
title: '{% trans "Add Supplier Part" %}',
fields: function(data) {
var fields = supplierPartFields({
part: data.part,
});
fields.supplier.value = options.supplier;
// Adjust manufacturer part query based on selected part
fields.manufacturer_part.adjustFilters = function(query, opts) {
var part = getFormFieldValue('part', {}, opts);
if (part) {
query.part = part;
}
return query;
};
return fields;
}
}
},
quantity: {},
reference: {},
purchase_price: {
icon: 'fa-dollar-sign',
},
purchase_price_currency: {
icon: 'fa-coins',
},
target_date: {
icon: 'fa-calendar-alt',
},
destination: {
icon: 'fa-sitemap',
filters: {
structural: false,
}
},
notes: {
icon: 'fa-sticky-note',
},
link: {
icon: 'fa-link',
}
};
if (options.order) {
fields.order.value = options.order;
fields.order.hidden = true;
}
if (options.currency) {
fields.purchase_price_currency.value = options.currency;
}
if (options.target_date) {
fields.target_date.value = options.target_date;
}
return fields;
}
// Create a new PurchaseOrderLineItem
function createPurchaseOrderLineItem(order, options={}) {
let fields = poLineItemFields({
order: order,
supplier: options.supplier,
currency: options.currency,
target_date: options.target_date,
update_pricing: true,
});
constructForm('{% url "api-po-line-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Add Line Item" %}',
onSuccess: function(response) {
handleFormSuccess(response, options);
}
});
}
/*
* Launches a modal form to mark a PurchaseOrder as "complete"
*/
function completePurchaseOrder(order_id, options={}) {
constructForm(
`/api/order/po/${order_id}/complete/`,
{
method: 'POST',
title: '{% trans "Complete Purchase Order" %}',
confirm: true,
fieldsFunction: function(opts) {
var fields = {
accept_incomplete: {},
};
if (opts.context.is_complete) {
delete fields['accept_incomplete'];
}
return fields;
},
preFormContent: function(opts) {
var html = `
<div class='alert alert-block alert-info'>
{% trans "Mark this order as complete?" %}
</div>`;
if (opts.context.is_complete) {
html += `
<div class='alert alert-block alert-success'>
{% trans "All line items have been received" %}
</div>`;
} else {
html += `
<div class='alert alert-block alert-warning'>
{% trans 'This order has line items which have not been marked as received.' %}</br>
{% trans 'Completing this order means that the order and line items will no longer be editable.' %}
</div>`;
}
return html;
},
onSuccess: function(response) {
handleFormSuccess(response, options);
}
}
);
}
/*
* Launches a modal form to mark a PurchaseOrder as 'cancelled'
*/
function cancelPurchaseOrder(order_id, options={}) {
constructForm(
`/api/order/po/${order_id}/cancel/`,
{
method: 'POST',
title: '{% trans "Cancel Purchase Order" %}',
confirm: true,
preFormContent: function(opts) {
var html = `
<div class='alert alert-info alert-block'>
{% trans "Are you sure you wish to cancel this purchase order?" %}
</div>`;
if (!opts.context.can_cancel) {
html += `
<div class='alert alert-danger alert-block'>
{% trans "This purchase order can not be cancelled" %}
</div>`;
}
return html;
},
onSuccess: function(response) {
handleFormSuccess(response, options);
}
}
);
}
/*
* Launches a modal form to mark a PurchaseOrder as "issued"
*/
function issuePurchaseOrder(order_id, options={}) {
let html = `
<div class='alert alert-block alert-warning'>
{% trans 'After placing this order, line items will no longer be editable.' %}
</div>`;
constructForm(`{% url "api-po-list" %}${order_id}/issue/`, {
method: 'POST',
title: '{% trans "Issue Purchase Order" %}',
confirm: true,
preFormContent: html,
onSuccess: function(response) {
handleFormSuccess(response, options);
}
});
}
function newSupplierPartFromOrderWizard(e) {
/* Create a new supplier part directly from an order form.
* Launches a secondary modal and (if successful),
* back-populates the selected row.
*/
e = e || window.event;
var src = e.srcElement || e.target;
var part = $(src).attr('part');
if (!part) {
part = $(src).closest('button').attr('part');
}
createSupplierPart({
part: part,
onSuccess: function(data) {
// TODO: 2021-08-23 - This whole form wizard needs to be refactored.
// In the future, use the API forms functionality to add the new item
// For now, this hack will have to do...
var dropdown = `#id_supplier_part_${part}`;
var pk = data.pk;
inventreeGet(
`/api/company/part/${pk}/`,
{
supplier_detail: true,
},
{
success: function(response) {
var text = '';
if (response.supplier_detail) {
text += response.supplier_detail.name;
text += ' | ';
}
text += response.SKU;
var option = new Option(text, pk, true, true);
$('#modal-form').find(dropdown).append(option).trigger('change');
}
}
);
}
});
}
/*
* Create a new form to order parts based on the list of provided parts.
*/
function orderParts(parts_list, options={}) {
var parts = [];
var parts_seen = {};
parts_list.forEach(function(part) {
if (part.purchaseable) {
// Prevent duplicates
if (!(part.pk in parts_seen)) {
parts_seen[part.pk] = true;
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);
// Default quantity value
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',
}
);
let buttons = '';
if (parts.length > 1) {
buttons += makeRemoveButton(
'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 = wrapButtons(buttons);
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;
}
// Remove a single row form this dialog
function removeRow(pk, opts) {
// Remove the row
$(opts.modal).find(`#order_row_${pk}`).remove();
// If the modal is now "empty", dismiss it
if (!($(opts.modal).find('.part-order-row').exists())) {
closeModal(opts.modal);
// If there is a onSuccess callback defined, call it
if (options && options.onSuccess) {
options.onSuccess();
}
}
}
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>
`;
// Construct API filters for the SupplierPart field
var supplier_part_filters = {
supplier_detail: true,
part_detail: true,
};
if (options.supplier) {
supplier_part_filters.supplier = options.supplier;
}
if (options.manufacturer) {
supplier_part_filters.manufacturer = options.manufacturer;
}
if (options.manufacturer_part) {
supplier_part_filters.manufacturer_part = options.manufacturer_part;
}
// Construct API filters for the PurchaseOrder field
var order_filters = {
status: {{ PurchaseOrderStatus.PENDING }},
supplier_detail: true,
};
if (options.supplier) {
order_filters.supplier = options.supplier;
}
constructFormBody({}, {
preFormContent: html,
title: '{% trans "Order Parts" %}',
hideSubmitButton: true,
closeText: '{% trans "Close" %}',
afterRender: function(fields, opts) {
parts.forEach(function(part) {
var pk = part.pk;
// Filter by base part
supplier_part_filters.part = pk;
if (part.manufacturer_part) {
// Filter by manufacturer part
supplier_part_filters.manufacturer_part = part.manufacturer_part;
}
// Callback function when supplier part is changed
// This is used to update the "pack size" attribute
var onSupplierPartChanged = function(value, name, field, opts) {
var pack_quantity = 1;
var units = '';
$(opts.modal).find(`#info-pack-size-${pk}`).remove();
if (typeof value === 'object') {
value = value.pk;
}
if (value != null) {
inventreeGet(
`/api/company/part/${value}/`,
{
part_detail: true,
},
{
success: function(response) {
pack_quantity = response.pack_quantity_native || 1;
units = response.part_detail.units || '';
}
}
).then(function() {
if (pack_quantity != 1) {
var txt = `<span class='fas fa-info-circle icon-blue'></span> {% trans "Pack Quantity" %}: ${pack_quantity} ${units}`;
$(opts.modal).find(`#id_quantity_${pk}`).after(`<div class='form-info-message' id='info-pack-size-${pk}'>${txt}</div>`);
}
});
}
};
var supplier_part_field = {
name: `part_${part.pk}`,
model: 'supplierpart',
api_url: '{% url "api-supplier-part-list" %}',
required: true,
type: 'related field',
auto_fill: true,
value: options.supplier_part,
filters: supplier_part_filters,
onEdit: onSupplierPartChanged,
noResults: function(query) {
return '{% trans "No matching supplier parts" %}';
}
};
// Configure the "supplier part" field
initializeRelatedField(supplier_part_field, null, opts);
addFieldCallback(`part_${part.pk}`, supplier_part_field, 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,
value: options.order,
filters: order_filters,
noResults: function(query) {
return '{% trans "No matching purchase orders" %}';
}
}, null, opts);
// Request 'requirements' information for each part
inventreeGet(`{% url "api-part-list" %}${part.pk}/requirements/`, {}, {
success: function(response) {
var required = response.required || 0;
var allocated = response.allocated || 0;
var available = response.available_stock || 0;
// Based on what we currently 'have' on hand, what do we need to order?
var deficit = Math.max(required - allocated, 0);
if (available < deficit) {
var q = deficit - available;
updateFieldValue(
`quantity_${part.pk}`,
q,
{},
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) {
removeRow(pk, opts);
},
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');
removeRow(pk, opts);
});
// 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
);
}
});
});
}
});
}
/* Create a new purchase order directly from an order form.
* Launches a secondary modal and (if successful),
* back-fills the newly created purchase order.
*/
function newPurchaseOrderFromOrderWizard(e) {
e = e || window.event;
var src = e.target || e.srcElement;
var supplier = $(src).attr('supplierid');
createPurchaseOrder({
supplier: supplier,
onSuccess: function(data) {
// TODO: 2021-08-23 - The whole form wizard needs to be refactored
// In the future, the drop-down should be using a dynamic AJAX request
// to fill out the select2 options!
var pk = data.pk;
inventreeGet(
`/api/order/po/${pk}/`,
{
supplier_detail: true,
},
{
success: function(response) {
var text = response.reference;
if (response.supplier_detail) {
text += ` ${response.supplier_detail.name}`;
}
var dropdown = `#id-purchase-order-${supplier}`;
var option = new Option(text, pk, true, true);
$('#modal-form').find(dropdown).append(option).trigger('change');
}
}
);
}
});
}
/**
* Receive stock items against a PurchaseOrder
* Uses the PurchaseOrderReceive API endpoint
*
* arguments:
* - order_id, ID / PK for the PurchaseOrder instance
* - line_items: A list of PurchaseOrderLineItems objects to be allocated
*
* options:
* -
*/
function receivePurchaseOrderItems(order_id, line_items, options={}) {
// Zero items selected?
if (line_items.length == 0) {
showAlertDialog(
'{% trans "Select Line Items" %}',
'{% trans "At least one line item must be selected" %}',
);
return;
}
function renderLineItem(line_item, opts={}) {
var pk = line_item.pk;
// Part thumbnail + description
var thumb = thumbnailImage(line_item.part_detail.thumbnail);
var quantity = (line_item.quantity || 0) - (line_item.received || 0);
if (quantity < 0) {
quantity = 0;
}
var units = line_item.part_detail.units || '';
let pack_quantity = line_item.supplier_part_detail.pack_quantity;
let native_pack_quantity = line_item.supplier_part_detail.pack_quantity_native || 1;
let pack_size_div = '';
var received = quantity * native_pack_quantity;
if (native_pack_quantity != 1) {
pack_size_div = `
<div class='alert alert-small alert-block alert-info'>
{% trans "Pack Quantity" %}: ${pack_quantity}<br>
{% trans "Received Quantity" %}: <span class='pack_received_quantity' id='items_received_quantity_${pk}'>${received}</span> ${units}
</div>`;
}
// Quantity to Receive
var quantity_input = constructField(
`items_quantity_${pk}`,
{
type: 'decimal',
min_value: 0,
value: quantity,
title: '{% trans "Quantity to receive" %}',
required: true,
},
{
hideLabels: true,
}
);
// Add in options for "batch code" and "serial numbers"
var batch_input = constructField(
`items_batch_code_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Batch Code" %}',
help_text: '{% trans "Enter batch code for incoming stock items" %}',
icon: 'fa-layer-group',
},
{
hideLabels: true,
}
);
// Hidden barcode input
var barcode_input = constructField(
`items_barcode_${pk}`,
{
type: 'string',
required: 'false',
hidden: 'true'
}
);
var sn_input = constructField(
`items_serial_numbers_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Serial Numbers" %}',
help_text: '{% trans "Enter serial numbers for incoming stock items" %}',
icon: 'fa-hashtag',
},
{
hideLabels: true,
}
);
var quantity_input_group = `${quantity_input}${pack_size_div}`;
// Construct list of StockItem status codes
var choices = [];
for (var key in stockCodes) {
choices.push({
value: stockCodes[key].key,
display_name: stockCodes[key].value,
});
}
var destination_input = constructField(
`items_location_${pk}`,
{
type: 'related field',
label: '{% trans "Location" %}',
required: false,
icon: 'fa-sitemap',
},
{
hideLabels: true,
}
);
var status_input = constructField(
`items_status_${pk}`,
{
type: 'choice',
label: '{% trans "Stock Status" %}',
required: true,
choices: choices,
value: 10, // OK
},
{
hideLabels: true,
}
);
// Button to remove the row
let buttons = '';
if (global_settings.BARCODE_ENABLE) {
buttons += makeIconButton('fa-qrcode', 'button-row-add-barcode', pk, '{% trans "Add barcode" %}');
buttons += makeIconButton('fa-unlink icon-red', 'button-row-remove-barcode', pk, '{% trans "Remove barcode" %}', {hidden: true});
}
buttons += makeIconButton('fa-sitemap', 'button-row-add-location', pk, '{% trans "Specify location" %}', {
collapseTarget: `row-destination-${pk}`
});
buttons += makeIconButton(
'fa-layer-group',
'button-row-add-batch',
pk,
'{% trans "Add batch code" %}',
{
collapseTarget: `row-batch-${pk}`
}
);
if (line_item.part_detail.trackable) {
buttons += makeIconButton(
'fa-hashtag',
'button-row-add-serials',
pk,
'{% trans "Add serial numbers" %}',
{
collapseTarget: `row-serials-${pk}`,
}
);
}
if (line_items.length > 1) {
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
}
buttons = wrapButtons(buttons);
let progress = makeProgressBar(line_item.received, line_item.quantity);
var html = `
<tr id='receive_row_${pk}' class='stock-receive-row'>
<td id='part_${pk}'>
${thumb} ${line_item.part_detail.full_name}
</td>
<td id='sku_${pk}'>
${line_item.supplier_part_detail.SKU}
</td>
<td id='received_${pk}'>
${progress}
</td>
<td id='quantity_${pk}'>
${quantity_input_group}
</td>
<td id='status_${pk}'>
${status_input}
</td>
<td id='actions_${pk}'>
${barcode_input}
${buttons}
</td>
</tr>
<!-- Hidden rows for extra data entry -->
<tr id='row-destination-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Location" %}</th>
<td colspan='2'>${destination_input}</td>
<td></td>
</tr>
<tr id='row-batch-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Batch" %}</th>
<td colspan='2'>${batch_input}</td>
<td></td>
</tr>
<tr id='row-serials-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Serials" %}</th>
<td colspan=2'>${sn_input}</td>
<td></td>
</tr>
`;
return html;
}
var table_entries = '';
line_items.forEach(function(item) {
if (item.received < item.quantity) {
table_entries += renderLineItem(item);
}
});
var html = ``;
// Add table
html += `
<table class='table table-condensed' id='order-receive-table'>
<thead>
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Order Code" %}</th>
<th>{% trans "Received" %}</th>
<th style='min-width: 50px;'>{% trans "Quantity to Receive" %}</th>
<th style='min-width: 150px;'>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
${table_entries}
</tbody>
</table>
`;
constructForm(`{% url "api-po-list" %}${order_id}/receive/`, {
method: 'POST',
fields: {
location: {
filters: {
structural: false,
}
},
},
preFormContent: html,
confirm: true,
confirmMessage: '{% trans "Confirm receipt of items" %}',
title: '{% trans "Receive Purchase Order Items" %}',
afterRender: function(fields, opts) {
// Run initialization routines for each line in the form
line_items.forEach(function(item) {
var pk = item.pk;
var name = `items_location_${pk}`;
var field_details = {
name: name,
api_url: '{% url "api-location-list" %}',
filters: {
},
type: 'related field',
model: 'stocklocation',
required: false,
auto_fill: false,
value: item.destination || item.part_detail.default_location,
render_description: false,
};
// Initialize the location field
initializeRelatedField(
field_details,
null,
opts,
);
// Add 'clear' button callback for the location field
addClearCallback(
name,
field_details,
opts
);
// Setup stock item status field
initializeChoiceField(
{
name: `items_status_${pk}`,
},
null,
opts
);
// Add change callback for quantity field
if (item.supplier_part_detail.pack_quantity_native != 1) {
$(opts.modal).find(`#id_items_quantity_${pk}`).change(function() {
var value = $(opts.modal).find(`#id_items_quantity_${pk}`).val();
var el = $(opts.modal).find(`#quantity_${pk}`).find('.pack_received_quantity');
var actual = value * item.supplier_part_detail.pack_quantity_native;
actual = formatDecimal(actual);
el.text(actual);
});
}
});
// Add callbacks to add barcode
if (global_settings.BARCODE_ENABLE) {
$(opts.modal).find('.button-row-add-barcode').click(function() {
var btn = $(this);
let pk = btn.attr('pk');
// Scan to see if the barcode matches an existing StockItem
barcodeDialog('{% trans "Scan Item Barcode" %}', {
details: '{% trans "Scan barcode on incoming item (must not match any existing stock items)" %}',
onScan: function(response, barcode_options) {
// A 'success' result means that the barcode matches something existing in the database
showBarcodeMessage(barcode_options.modal, '{% trans "Barcode matches existing item" %}');
},
onError400: function(response, barcode_options) {
if (response.barcode_data && response.barcode_hash) {
// Success! Hide the modal and update the value
$(barcode_options.modal).modal('hide');
btn.hide();
$(opts.modal).find(`#button-row-remove-barcode-${pk}`).show();
updateFieldValue(`items_barcode_${pk}`, response.barcode_data, {}, opts);
} else {
showBarcodeMessage(barcode_options.modal, '{% trans "Invalid barcode data" %}');
}
}
});
});
$(opts.modal).find('.button-row-remove-barcode').click(function() {
var btn = $(this);
let pk = btn.attr('pk');
btn.hide();
$(opts.modal).find(`#button-row-add-barcode-${pk}`).show();
updateFieldValue(`items_barcode_${pk}`, '', {}, opts);
});
}
// Add callbacks to remove rows
$(opts.modal).find('.button-row-remove').click(function() {
var pk = $(this).attr('pk');
$(opts.modal).find(`#receive_row_${pk}`).remove();
});
},
onSubmit: function(fields, opts) {
// Extract data elements from the form
var data = {
items: [],
location: getFormFieldValue('location', {}, opts),
};
var item_pk_values = [];
line_items.forEach(function(item) {
var pk = item.pk;
// Extract data for each line
var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
var status = getFormFieldValue(`items_status_${pk}`, {}, opts);
var location = getFormFieldValue(`items_location_${pk}`, {}, opts);
if (quantity != null) {
var line = {
line_item: pk,
quantity: quantity,
status: status,
location: location,
};
if (global_settings.BARCODE_ENABLE) {
line.barcode = getFormFieldValue(`items_barcode_${pk}`, {}, opts);
}
if (getFormFieldElement(`items_batch_code_${pk}`).exists()) {
line.batch_code = getFormFieldValue(`items_batch_code_${pk}`);
}
if (getFormFieldElement(`items_serial_numbers_${pk}`).exists()) {
line.serial_numbers = getFormFieldValue(`items_serial_numbers_${pk}`);
}
data.items.push(line);
item_pk_values.push(pk);
}
});
// Provide list of nested values
opts.nested = {
'items': item_pk_values,
};
inventreePut(
opts.url,
data,
{
method: 'POST',
success: function(response) {
// Hide the modal
$(opts.modal).modal('hide');
if (options.success) {
options.success(response);
}
},
error: function(xhr) {
switch (xhr.status) {
case 400:
handleFormErrors(xhr.responseJSON, fields, opts);
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr, opts.url);
break;
}
}
}
);
}
});
}
/*
* Edit a purchase order line item in a modal form.
*/
function editPurchaseOrderLineItem(e) {
e = e || window.event;
var src = e.target || e.srcElement;
var url = $(src).attr('url');
// TODO: Migrate this to the API forms
launchModalForm(url, {
reload: true,
});
}
/*
* Delete a purchase order line item in a modal form
*/
function removePurchaseOrderLineItem(e) {
e = e || window.event;
var src = e.target || e.srcElement;
var url = $(src).attr('url');
// TODO: Migrate this to the API forms
launchModalForm(url, {
reload: true,
});
}
/*
* Load a table displaying list of purchase orders
*/
function loadPurchaseOrderTable(table, options) {
// Ensure the table starts in a known state
$(table).bootstrapTable('destroy');
options.params = options.params || {};
options.params['supplier_detail'] = true;
var filters = loadTableFilters('purchaseorder', options.params);
setupFilterList('purchaseorder', $(table), '#filter-list-purchaseorder', {
download: true,
report: {
url: '{% url "api-po-report-list" %}',
key: 'order',
}
});
var display_mode = inventreeLoad('purchaseorder-table-display-mode', 'list');
// Function for rendering PurchaseOrder calendar display
function buildEvents(calendar) {
var start = startDate(calendar);
var end = endDate(calendar);
clearEvents(calendar);
// Extract current filters from table
var table_options = $(table).bootstrapTable('getOptions');
var filters = table_options.query_params || {};
filters.supplier_detail = true;
filters.min_date = start;
filters.max_date = end;
// Request purchase orders from the server within specified date range
inventreeGet(
'{% url "api-po-list" %}',
filters,
{
success: function(response) {
for (var idx = 0; idx < response.length; idx++) {
var order = response[idx];
var date = order.creation_date;
if (order.complete_date) {
date = order.complete_date;
} else if (order.target_date) {
date = order.target_date;
}
var title = `${order.reference} - ${order.supplier_detail.name}`;
var color = '#4c68f5';
if (order.complete_date) {
color = '#25c235';
} else if (order.overdue) {
color = '#c22525';
} else {
color = '#4c68f5';
}
var event = {
title: title,
start: date,
end: date,
url: `/order/purchase-order/${order.pk}/`,
backgroundColor: color,
};
calendar.addEvent(event);
}
}
}
);
}
$(table).inventreeTable({
url: '{% url "api-po-list" %}',
queryParams: filters,
name: 'purchaseorder',
groupBy: false,
sidePagination: 'server',
original: options.params,
showColumns: display_mode == 'list',
disablePagination: display_mode == 'calendar',
showCustomViewButton: false,
showCustomView: display_mode == 'calendar',
search: display_mode != 'calendar',
formatNoMatches: function() {
return '{% trans "No purchase orders found" %}';
},
buttons: constructOrderTableButtons({
prefix: 'purchaseorder',
disableTreeView: true,
callback: function() {
// Reload the entire table
loadPurchaseOrderTable(table, options);
}
}),
columns: [
{
title: '',
visible: true,
checkbox: true,
switchable: false,
},
{
field: 'reference',
title: '{% trans "Purchase Order" %}',
sortable: true,
switchable: false,
formatter: function(value, row) {
var html = renderLink(value, `/order/purchase-order/${row.pk}/`);
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
}
return html;
}
},
{
field: 'supplier_detail',
title: '{% trans "Supplier" %}',
sortable: true,
sortName: 'supplier__name',
formatter: function(value, row) {
return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/?display=purchase-orders`);
}
},
{
field: 'supplier_reference',
title: '{% trans "Supplier Reference" %}',
},
{
field: 'description',
title: '{% trans "Description" %}',
},
{
field: 'project_code',
title: '{% trans "Project Code" %}',
switchable: global_settings.PROJECT_CODES_ENABLED,
visible: global_settings.PROJECT_CODES_ENABLED,
sortable: true,
formatter: function(value, row) {
if (row.project_code_detail) {
return `<span title='${row.project_code_detail.description}'>${row.project_code_detail.code}</span>`;
}
}
},
{
field: 'status',
title: '{% trans "Status" %}',
switchable: true,
sortable: true,
formatter: function(value, row) {
return purchaseOrderStatusDisplay(row.status);
}
},
{
field: 'creation_date',
title: '{% trans "Date" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
{
field: 'target_date',
title: '{% trans "Target Date" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
{
field: 'line_items',
title: '{% trans "Items" %}',
sortable: true,
},
{
field: 'total_price',
title: '{% trans "Total Cost" %}',
switchable: true,
sortable: true,
formatter: function(value, row) {
return formatCurrency(value, {
currency: row.order_currency,
});
},
},
{
field: 'responsible',
title: '{% trans "Responsible" %}',
switchable: true,
sortable: true,
formatter: function(value, row) {
if (!row.responsible_detail) {
return '-';
}
var html = row.responsible_detail.name;
if (row.responsible_detail.label == 'group') {
html += `<span class='float-right fas fa-users'></span>`;
} else {
html += `<span class='float-right fas fa-user'></span>`;
}
return html;
}
},
],
customView: function(data) {
return `<div id='purchase-order-calendar'></div>`;
},
onLoadSuccess: function() {
if (display_mode == 'calendar') {
let el = document.getElementById('purchase-order-calendar');
let calendar = new FullCalendar.Calendar(el, {
initialView: 'dayGridMonth',
nowIndicator: true,
aspectRatio: 2.5,
locale: options.locale,
datesSet: function() {
buildEvents(calendar);
}
});
calendar.render();
}
}
});
}
/*
* Delete the selected Purchase Order Line Items from the database
*/
function deletePurchaseOrderLineItems(items, options={}) {
function renderItem(item, opts={}) {
var part = item.part_detail;
var thumb = thumbnailImage(item.part_detail.thumbnail || item.part_detail.image);
var MPN = item.supplier_part_detail.manufacturer_part_detail ? item.supplier_part_detail.manufacturer_part_detail.MPN : '-';
var html = `
<tr>
<td>${thumb} ${part.full_name}</td>
<td>${part.description}</td>
<td>${item.supplier_part_detail.SKU}</td>
<td>${MPN}</td>
<td>${item.quantity}
</tr>
`;
return html;
}
var rows = '';
var ids = [];
items.forEach(function(item) {
rows += renderItem(item);
ids.push(item.pk);
});
var html = `
<div class='alert alert-block alert-danger'>
{% trans "All selected Line items will be deleted" %}
</div>
<table class='table table-striped table-condensed'>
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "SKU" %}</th>
<th>{% trans "MPN" %}</th>
<th>{% trans "Quantity" %}</th>
</tr>
${rows}
</table>
`;
constructForm('{% url "api-po-line-list" %}', {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete selected Line items?" %}',
form_data: {
items: ids,
},
preFormContent: html,
refreshTable: '#po-line-table',
});
}
/**
* Load a table displaying line items for a particular PurchasesOrder
* @param {String} table - HTML ID tag e.g. '#table'
* @param {Object} options - options which must provide:
* - order (integer PK)
* - supplier (integer PK)
* - allow_edit (boolean)
* - allow_receive (boolean)
*/
function loadPurchaseOrderLineItemTable(table, options={}) {
options.params = options.params || {};
options.params['order'] = options.order;
options.params['part_detail'] = true;
// Override 'editing' if order is not pending
if (!options.pending && !global_settings.PURCHASEORDER_EDIT_COMPLETED_ORDERS) {
options.allow_edit = false;
}
var filters = loadTableFilters('purchaseorderlineitem', options.params);
setupFilterList('purchaseorderlineitem', $(table), options.filter_target || '#filter-list-purchase-order-lines', {
download: true,
});
function setupCallbacks() {
if (options.allow_edit) {
// Callback for "duplicate" button
$(table).find('.button-line-duplicate').click(function() {
var pk = $(this).attr('pk');
inventreeGet(`{% url "api-po-line-list" %}${pk}/`, {}, {
success: function(data) {
var fields = poLineItemFields({
supplier: options.supplier,
});
constructForm('{% url "api-po-line-list" %}', {
method: 'POST',
fields: fields,
data: data,
title: '{% trans "Duplicate Line Item" %}',
refreshTable: table,
});
}
});
});
// Callback for "edit" button
$(table).find('.button-line-edit').click(function() {
var pk = $(this).attr('pk');
var fields = poLineItemFields(options);
constructForm(`{% url "api-po-line-list" %}${pk}/`, {
fields: fields,
title: '{% trans "Edit Line Item" %}',
refreshTable: table,
});
});
// Callback for "delete" button
$(table).find('.button-line-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`{% url "api-po-line-list" %}${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Line Item" %}',
refreshTable: table,
});
});
// Callback for bulk deleting multiple lines
$('#po-lines-bulk-delete').off('click').on('click', function() {
var rows = getTableData(' #po-line-table');
deletePurchaseOrderLineItems(rows);
});
}
if (options.allow_receive) {
$(table).find('.button-line-receive').click(function() {
var pk = $(this).attr('pk');
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
if (!line_item) {
console.warn('getRowByUniqueId returned null');
return;
}
receivePurchaseOrderItems(
options.order,
[
line_item,
],
{
success: function() {
// Reload the line item table
reloadBootstrapTable(table);
// Reload the "received stock" table
reloadBootstrapTable('#stock-table');
}
}
);
});
}
}
$(table).inventreeTable({
onPostBody: setupCallbacks,
name: 'purchaseorderlines',
sidePagination: 'server',
formatNoMatches: function() {
return '{% trans "No line items found" %}';
},
queryParams: filters,
original: options.params,
url: '{% url "api-po-line-list" %}',
showFooter: true,
uniqueId: 'pk',
columns: [
{
checkbox: true,
visible: true,
switchable: false,
},
{
field: 'part',
sortable: true,
sortName: 'part_name',
title: '{% trans "Part" %}',
switchable: false,
formatter: function(value, row, index, field) {
if (row.part) {
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
} else {
return '-';
}
},
footerFormatter: function() {
return '{% trans "Total" %}';
}
},
{
field: 'part_detail.description',
title: '{% trans "Description" %}',
},
{
sortable: true,
sortName: 'SKU',
field: 'supplier_part_detail.SKU',
title: '{% trans "SKU" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderClipboard(renderLink(value, `/supplier-part/${row.part}/`));
} else {
return '-';
}
},
},
{
sortable: false,
field: 'supplier_part_detail.link',
title: '{% trans "Link" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value, {external: true});
} else {
return '';
}
},
},
{
sortable: true,
sortName: 'MPN',
field: 'supplier_part_detail.manufacturer_part_detail.MPN',
title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
if (row.supplier_part_detail && row.supplier_part_detail.manufacturer_part) {
return renderClipboard(renderLink(value, `/manufacturer-part/${row.supplier_part_detail.manufacturer_part}/`));
} else {
return '-';
}
},
},
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
},
{
sortable: true,
switchable: false,
field: 'quantity',
title: '{% trans "Quantity" %}',
formatter: function(value, row) {
let units = '';
if (row.part_detail && row.part_detail.units) {
units = ` ${row.part_detail.units}`;
}
let data = value;
if (row.supplier_part_detail && row.supplier_part_detail.pack_quantity_native != 1.0) {
let pack_quantity = row.supplier_part_detail.pack_quantity;
let total = value * row.supplier_part_detail.pack_quantity_native;
data += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Pack Quantity" %}: ${pack_quantity} - {% trans "Total Quantity" %}: ${total}${units}'></span>`;
}
return data;
},
footerFormatter: function(data) {
return data.map(function(row) {
return +row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
}
},
{
sortable: false,
switchable: true,
field: 'supplier_part_detail.pack_quantity',
title: '{% trans "Pack Quantity" %}',
formatter: function(value, row) {
var units = row.part_detail.units;
if (units) {
value += ` ${units}`;
}
return value;
}
},
{
sortable: true,
field: 'purchase_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
return formatCurrency(row.purchase_price, {
currency: row.purchase_price_currency,
});
}
},
{
field: 'total_price',
sortable: true,
title: '{% trans "Total Price" %}',
formatter: function(value, row) {
return formatCurrency(row.purchase_price * row.quantity, {
currency: row.purchase_price_currency
});
},
footerFormatter: function(data) {
return calculateTotalPrice(
data,
function(row) {
return row.purchase_price ? row.purchase_price * row.quantity : null;
},
function(row) {
return row.purchase_price_currency;
}
);
}
},
{
sortable: true,
field: 'target_date',
switchable: true,
title: '{% trans "Target Date" %}',
formatter: function(value, row) {
if (row.target_date) {
var html = renderDate(row.target_date);
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "This line item is overdue" %}');
}
return html;
} else if (row.order_detail && row.order_detail.target_date) {
return `<em>${renderDate(row.order_detail.target_date)}</em>`;
} else {
return '-';
}
}
},
{
sortable: false,
field: 'received',
switchable: false,
title: '{% trans "Received" %}',
formatter: function(value, row, index, field) {
return makeProgressBar(row.received, row.quantity, {
id: `order-line-progress-${row.pk}`,
});
},
sorter: function(valA, valB, rowA, rowB) {
if (rowA.received == 0 && rowB.received == 0) {
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = parseFloat(rowA.received) / rowA.quantity;
var progressB = parseFloat(rowB.received) / rowB.quantity;
return (progressA < progressB) ? 1 : -1;
}
},
{
field: 'destination',
title: '{% trans "Destination" %}',
formatter: function(value, row) {
if (value) {
return renderLink(row.destination_detail.pathstring, `/stock/location/${value}/`);
} else {
return '-';
}
}
},
{
field: 'notes',
title: '{% trans "Notes" %}',
},
{
field: 'link',
title: '{% trans "Link" %}',
formatter: function(value) {
if (value) {
return renderLink(value, value);
}
}
},
{
switchable: false,
field: 'buttons',
title: '',
formatter: function(value, row, index, field) {
let buttons = '';
let pk = row.pk;
if (options.allow_receive && row.received < row.quantity) {
buttons += makeIconButton('fa-sign-in-alt icon-green', 'button-line-receive', pk, '{% trans "Receive line item" %}');
}
if (options.allow_edit) {
buttons += makeCopyButton('button-line-duplicate', pk, '{% trans "Duplicate line item" %}');
buttons += makeEditButton('button-line-edit', pk, '{% trans "Edit line item" %}');
buttons += makeDeleteButton('button-line-delete', pk, '{% trans "Delete line item" %}');
}
return wrapButtons(buttons);
},
}
]
});
}