2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 12:36:45 +00:00
2021-10-05 16:10:46 +11:00

1118 lines
33 KiB
JavaScript

{% load i18n %}
{% load inventree_extras %}
/* globals
companyFormFields,
constructForm,
createSupplierPart,
global_settings,
imageHoverIcon,
inventreeGet,
launchModalForm,
loadTableFilters,
makeIconBadge,
purchaseOrderStatusDisplay,
receivePurchaseOrderItems,
renderLink,
salesOrderStatusDisplay,
setupFilterList,
*/
/* exported
createSalesOrder,
editPurchaseOrderLineItem,
loadPurchaseOrderLineItemTable,
loadPurchaseOrderTable,
loadSalesOrderAllocationTable,
loadSalesOrderTable,
newPurchaseOrderFromOrderWizard,
newSupplierPartFromOrderWizard,
removeOrderRowFromOrderWizard,
removePurchaseOrderLineItem,
*/
// Create a new SalesOrder
function createSalesOrder(options={}) {
constructForm('{% url "api-so-list" %}', {
method: 'POST',
fields: {
reference: {
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
},
customer: {
value: options.customer,
secondary: {
title: '{% trans "Add Customer" %}',
fields: function() {
var fields = companyFormFields();
fields.is_customer.value = true;
return fields;
}
}
},
customer_reference: {},
description: {},
target_date: {
icon: 'fa-calendar-alt',
},
link: {
icon: 'fa-link',
},
responsible: {
icon: 'fa-user',
}
},
onSuccess: function(data) {
location.href = `/order/sales-order/${data.pk}/`;
},
title: '{% trans "Create Sales Order" %}',
});
}
// Create a new PurchaseOrder
function createPurchaseOrder(options={}) {
constructForm('{% url "api-po-list" %}', {
method: 'POST',
fields: {
reference: {
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
supplier: {
value: options.supplier,
secondary: {
title: '{% trans "Add Supplier" %}',
fields: function() {
var fields = companyFormFields();
fields.is_supplier.value = true;
return fields;
}
}
},
supplier_reference: {},
description: {},
target_date: {
icon: 'fa-calendar-alt',
},
link: {
icon: 'fa-link',
},
responsible: {
icon: 'fa-user',
}
},
onSuccess: function(data) {
if (options.onSuccess) {
options.onSuccess(data);
} else {
// Default action is to redirect browser to the new PO
location.href = `/order/purchase-order/${data.pk}/`;
}
},
title: '{% trans "Create Purchase Order" %}',
});
}
function removeOrderRowFromOrderWizard(e) {
/* Remove a part selection from an order form. */
e = e || window.event;
var src = e.target || e.srcElement;
var row = $(src).attr('row');
$('#' + row).remove();
}
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');
}
}
);
}
});
}
function newPurchaseOrderFromOrderWizard(e) {
/* Create a new purchase order directly from an order form.
* Launches a secondary modal and (if successful),
* back-fills the newly created purchase order.
*/
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 = global_settings.PURCHASEORDER_REFERENCE_PREFIX || '';
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 POReceive 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={}) {
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;
}
// 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,
}
);
// Construct list of StockItem status codes
var choices = [];
for (var key in stockCodes) {
choices.push({
value: key,
display_name: stockCodes[key].value,
});
}
var destination_input = constructField(
`items_location_${pk}`,
{
type: 'related field',
label: '{% trans "Location" %}',
required: false,
},
{
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
var delete_button = `<div class='btn-group float-right' role='group'>`;
delete_button += makeIconButton(
'fa-times icon-red',
'button-row-remove',
pk,
'{% trans "Remove row" %}',
);
delete_button += '</div>';
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='on_order_${pk}'>
${line_item.quantity}
</td>
<td id='received_${pk}'>
${line_item.received}
</td>
<td id='quantity_${pk}'>
${quantity_input}
</td>
<td id='status_${pk}'>
${status_input}
</td>
<td id='desination_${pk}'>
${destination_input}
</td>
<td id='actions_${pk}'>
${delete_button}
</td>
</tr>`;
return html;
}
var table_entries = '';
line_items.forEach(function(item) {
table_entries += renderLineItem(item);
});
var html = ``;
// Add table
html += `
<table class='table table-striped table-condensed' id='order-receive-table'>
<thead>
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Order Code" %}</th>
<th>{% trans "Ordered" %}</th>
<th>{% trans "Received" %}</th>
<th style='min-width: 50px;'>{% trans "Receive" %}</th>
<th style='min-width: 150px;'>{% trans "Status" %}</th>
<th style='min-width: 300px;'>{% trans "Destination" %}</th>
<th></th>
</tr>
</thead>
<tbody>
${table_entries}
</tbody>
</table>
`;
constructForm(`/api/order/po/${order_id}/receive/`, {
method: 'POST',
fields: {
location: {},
},
preFormContent: html,
confirm: true,
confirmMessage: '{% trans "Confirm receipt of items" %}',
title: '{% trans "Receive Purchase Order Items" %}',
afterRender: function(fields, opts) {
// Initialize the "destination" field for each item
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,
};
initializeRelatedField(
field_details,
null,
opts,
);
addClearCallback(
name,
field_details,
opts
);
initializeChoiceField(
{
name: `items_status_${pk}`,
},
null,
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;
var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
var status = getFormFieldValue(`items_status_${pk}`, {}, opts);
var location = getFormFieldValue(`items_location_${pk}`, {}, opts);
if (quantity != null) {
data.items.push({
line_item: pk,
quantity: quantity,
status: status,
location: location,
});
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);
break;
}
}
}
);
}
});
}
function editPurchaseOrderLineItem(e) {
/* Edit a purchase order line item in a modal form.
*/
e = e || window.event;
var src = e.target || e.srcElement;
var url = $(src).attr('url');
launchModalForm(url, {
reload: true,
});
}
function removePurchaseOrderLineItem(e) {
/* Delete a purchase order line item in a modal form
*/
e = e || window.event;
var src = e.target || e.srcElement;
var url = $(src).attr('url');
launchModalForm(url, {
reload: true,
});
}
function loadPurchaseOrderTable(table, options) {
/* Create a purchase-order table */
options.params = options.params || {};
options.params['supplier_detail'] = true;
var filters = loadTableFilters('purchaseorder');
for (var key in options.params) {
filters[key] = options.params[key];
}
options.url = options.url || '{% url "api-po-list" %}';
setupFilterList('purchaseorder', $(table));
$(table).inventreeTable({
url: options.url,
queryParams: filters,
name: 'purchaseorder',
groupBy: false,
sidePagination: 'server',
original: options.params,
formatNoMatches: function() {
return '{% trans "No purchase orders found" %}';
},
columns: [
{
title: '',
visible: true,
checkbox: true,
switchable: false,
},
{
field: 'reference',
title: '{% trans "Purchase Order" %}',
sortable: true,
switchable: false,
formatter: function(value, row) {
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
}
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}/purchase-orders/`);
}
},
{
field: 'supplier_reference',
title: '{% trans "Supplier Reference" %}',
},
{
field: 'description',
title: '{% trans "Description" %}',
},
{
field: 'status',
title: '{% trans "Status" %}',
sortable: true,
formatter: function(value, row) {
return purchaseOrderStatusDisplay(row.status, row.status_text);
}
},
{
field: 'creation_date',
title: '{% trans "Date" %}',
sortable: true,
},
{
field: 'target_date',
title: '{% trans "Target Date" %}',
sortable: true,
},
{
field: 'line_items',
title: '{% trans "Items" %}',
sortable: true,
},
],
});
}
/**
* 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={}) {
function setupCallbacks() {
if (options.allow_edit) {
$(table).find('.button-line-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-line/${pk}/`, {
fields: {
part: {
filters: {
part_detail: true,
supplier_detail: true,
supplier: options.supplier,
}
},
quantity: {},
reference: {},
purchase_price: {},
purchase_price_currency: {},
destination: {},
notes: {},
},
title: '{% trans "Edit Line Item" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-line-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-line/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Line Item" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
}
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.log('WARNING: getRowByUniqueId returned null');
return;
}
receivePurchaseOrderItems(
options.order,
[
line_item,
],
{
success: function() {
$(table).bootstrapTable('refresh');
}
}
);
});
}
}
$(table).inventreeTable({
onPostBody: setupCallbacks,
name: 'purchaseorderlines',
sidePagination: 'server',
formatNoMatches: function() {
return '{% trans "No line items found" %}';
},
queryParams: {
order: options.order,
part_detail: true
},
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 renderLink(value, `/supplier-part/${row.part}/`);
} 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 renderLink(value, `/manufacturer-part/${row.supplier_part_detail.manufacturer_part}/`);
} else {
return '-';
}
},
},
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
},
{
sortable: true,
field: 'quantity',
title: '{% trans "Quantity" %}',
footerFormatter: function(data) {
return data.map(function(row) {
return +row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
}
},
{
sortable: true,
field: 'purchase_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
return row.purchase_price_string || row.purchase_price;
}
},
{
field: 'total_price',
sortable: true,
field: 'total_price',
title: '{% trans "Total price" %}',
formatter: function(value, row) {
var total = row.purchase_price * row.quantity;
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
return formatter.format(total);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
return +row['purchase_price']*row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD';
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: currency
}
);
return formatter.format(total);
}
},
{
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" %}',
},
{
switchable: false,
field: 'buttons',
title: '',
formatter: function(value, row, index, field) {
var html = `<div class='btn-group'>`;
var pk = row.pk;
if (options.allow_edit) {
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
}
if (options.allow_receive && row.received < row.quantity) {
html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}');
}
html += `</div>`;
return html;
},
}
]
});
}
function loadSalesOrderTable(table, options) {
options.params = options.params || {};
options.params['customer_detail'] = true;
var filters = loadTableFilters('salesorder');
for (var key in options.params) {
filters[key] = options.params[key];
}
options.url = options.url || '{% url "api-so-list" %}';
setupFilterList('salesorder', $(table));
$(table).inventreeTable({
url: options.url,
queryParams: filters,
name: 'salesorder',
groupBy: false,
sidePagination: 'server',
original: options.params,
formatNoMatches: function() {
return '{% trans "No sales orders found" %}';
},
columns: [
{
title: '',
checkbox: true,
visible: true,
switchable: false,
},
{
sortable: true,
field: 'reference',
title: '{% trans "Sales Order" %}',
formatter: function(value, row) {
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
}
var html = renderLink(value, `/order/sales-order/${row.pk}/`);
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
}
return html;
},
},
{
sortable: true,
sortName: 'customer__name',
field: 'customer_detail',
title: '{% trans "Customer" %}',
formatter: function(value, row) {
if (!row.customer_detail) {
return '{% trans "Invalid Customer" %}';
}
return imageHoverIcon(row.customer_detail.image) + renderLink(row.customer_detail.name, `/company/${row.customer}/sales-orders/`);
}
},
{
sortable: true,
field: 'customer_reference',
title: '{% trans "Customer Reference" %}',
},
{
sortable: false,
field: 'description',
title: '{% trans "Description" %}',
},
{
sortable: true,
field: 'status',
title: '{% trans "Status" %}',
formatter: function(value, row) {
return salesOrderStatusDisplay(row.status, row.status_text);
}
},
{
sortable: true,
field: 'creation_date',
title: '{% trans "Creation Date" %}',
},
{
sortable: true,
field: 'target_date',
title: '{% trans "Target Date" %}',
},
{
sortable: true,
field: 'shipment_date',
title: '{% trans "Shipment Date" %}',
},
{
sortable: true,
field: 'line_items',
title: '{% trans "Items" %}'
},
],
});
}
function loadSalesOrderAllocationTable(table, options={}) {
/**
* Load a table with SalesOrderAllocation items
*/
options.params = options.params || {};
options.params['location_detail'] = true;
options.params['part_detail'] = true;
options.params['item_detail'] = true;
options.params['order_detail'] = true;
var filters = loadTableFilters('salesorderallocation');
for (var key in options.params) {
filters[key] = options.params[key];
}
setupFilterList('salesorderallocation', $(table));
$(table).inventreeTable({
url: '{% url "api-so-allocation-list" %}',
queryParams: filters,
name: 'salesorderallocation',
groupBy: false,
search: false,
paginationVAlign: 'bottom',
original: options.params,
formatNoMatches: function() {
return '{% trans "No sales order allocations found" %}';
},
columns: [
{
field: 'pk',
visible: false,
switchable: false,
},
{
field: 'order',
switchable: false,
title: '{% trans "Order" %}',
formatter: function(value, row) {
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.order_detail.reference}`;
return renderLink(ref, `/order/sales-order/${row.order}/`);
}
},
{
field: 'item',
title: '{% trans "Stock Item" %}',
formatter: function(value, row) {
// Render a link to the particular stock item
var link = `/stock/item/${row.item}/`;
var text = `{% trans "Stock Item" %} ${row.item}`;
return renderLink(text, link);
}
},
{
field: 'location',
title: '{% trans "Location" %}',
formatter: function(value, row) {
if (!value) {
return '{% trans "Location not specified" %}';
}
var link = `/stock/location/${value}`;
var text = row.location_detail.description;
return renderLink(text, link);
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
sortable: true,
}
]
});
}