- `;
-
- if (!pending_shipments.length) {
- html += `
- {% trans "No pending shipments found" %}
- `;
- } else {
- html += `
- {% trans "No stock items have been allocated to pending shipments" %}
- `;
- }
-
- html += `
-
`;
-
- if (opts.context.is_complete) {
- html += `
-
- {% trans "All line items have been received" %}
-
`;
- } else {
- html += `
-
- {% trans 'This order has line items which have not been marked as received.' %}
- {% trans 'Completing this order means that the order and line items will no longer be editable.' %}
-
`;
- }
-
- 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 = `
-
- {% trans "Are you sure you wish to cancel this purchase order?" %}
-
`;
-
- if (!opts.context.can_cancel) {
- html += `
-
- {% trans "This purchase order can not be cancelled" %}
-
`;
- }
-
- return html;
- },
- onSuccess: function(response) {
- handleFormSuccess(response, options);
- }
- }
- );
-}
-
-
-/*
- * Launches a modal form to mark a PurchaseOrder as "issued"
- */
-function issuePurchaseOrder(order_id, options={}) {
-
- constructForm(
- `/api/order/po/${order_id}/issue/`,
- {
- method: 'POST',
- title: '{% trans "Issue Purchase Order" %}',
- confirm: true,
- preFormContent: function(opts) {
- var html = `
-
- {% trans 'After placing this purchase order, line items will no longer be editable.' %}
-
`;
-
- return html;
- },
- onSuccess: function(response) {
- handleFormSuccess(response, options);
- }
- }
- );
-}
-
-
-/*
- * Launches a modal form to mark a SalesOrder as "complete"
- */
-function completeSalesOrder(order_id, options={}) {
-
- constructForm(
- `/api/order/so/${order_id}/complete/`,
- {
- method: 'POST',
- title: '{% trans "Complete Sales 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 = `
-
- {% trans "Mark this order as complete?" %}
-
`;
-
- if (opts.context.pending_shipments) {
- html += `
-
- {% trans "Order cannot be completed as there are incomplete shipments" %}
-
`;
- }
-
- if (!opts.context.is_complete) {
- html += `
-
- {% trans "This order has line items which have not been completed." %}
- {% trans "Completing this order means that the order and line items will no longer be editable." %}
-
`;
- }
-
- return html;
- },
- onSuccess: function(response) {
- handleFormSuccess(response, options);
- }
- }
- );
-}
-
-
-/*
- * Launches a modal form to mark a SalesOrder as "cancelled"
- */
-function cancelSalesOrder(order_id, options={}) {
-
- constructForm(
- `/api/order/so/${order_id}/cancel/`,
- {
- method: 'POST',
- title: '{% trans "Cancel Sales Order" %}',
- confirm: true,
- preFormContent: function(opts) {
- var html = `
-
- {% trans "Cancelling this order means that the order will no longer be editable." %}
-
`;
-
- return html;
- },
- onSuccess: function(response) {
- handleFormSuccess(response, options);
- }
- }
- );
-}
-
-// Open a dialog to create a new sales order shipment
-function createSalesOrderShipment(options={}) {
-
- // Work out the next shipment number for the given order
- inventreeGet(
- '{% url "api-so-shipment-list" %}',
- {
- order: options.order,
- },
- {
- success: function(results) {
- // "predict" the next reference number
- var ref = results.length + 1;
-
- var found = false;
-
- while (!found) {
-
- var no_match = true;
-
- for (var ii = 0; ii < results.length; ii++) {
- if (ref.toString() == results[ii].reference.toString()) {
- no_match = false;
- break;
- }
- }
-
- if (no_match) {
- break;
- } else {
- ref++;
- }
- }
-
- var fields = salesOrderShipmentFields(options);
-
- fields.reference.value = ref;
- fields.reference.prefix = options.reference;
-
- constructForm('{% url "api-so-shipment-list" %}', {
- method: 'POST',
- fields: fields,
- title: '{% trans "Create New Shipment" %}',
- onSuccess: function(data) {
- if (options.onSuccess) {
- options.onSuccess(data);
- }
- }
- });
- }
- }
- );
-}
-
-
-function salesOrderFields(options={}) {
- let fields = {
- reference: {
- icon: 'fa-hashtag',
- },
- description: {},
- customer: {
- icon: 'fa-user-tie',
- secondary: {
- title: '{% trans "Add Customer" %}',
- fields: function() {
- var fields = companyFormFields();
- fields.is_customer.value = true;
- return fields;
- }
- }
- },
- customer_reference: {},
- target_date: {
- icon: 'fa-calendar-alt',
- },
- link: {
- icon: 'fa-link',
- },
- responsible: {
- icon: 'fa-user',
- }
- };
-
- return fields;
-}
-
-
-/*
- * Create a new SalesOrder
- */
-function createSalesOrder(options={}) {
-
- let fields = salesOrderFields(options);
-
- if (options.customer) {
- fields.customer.value = options.customer;
- }
-
- constructForm('{% url "api-so-list" %}', {
- method: 'POST',
- fields: fields,
- title: '{% trans "Create Sales Order" %}',
- onSuccess: function(data) {
- location.href = `/order/sales-order/${data.pk}/`;
- },
- });
-}
-
-
-/*
- * Edit an existing SalesOrder
- */
-function editSalesOrder(order_id, options={}) {
-
- constructForm(`/api/order/so/${order_id}/`, {
- fields: salesOrderFields(options),
- title: '{% trans "Edit Sales Order" %}',
- onSuccess: function(response) {
- handleFormSuccess(response, options);
- }
- });
-}
-
-
-/*
- * Launch a modal form to create a new SalesOrderLineItem
- */
-function createSalesOrderLineItem(options={}) {
-
- let fields = soLineItemFields(options);
-
- constructForm('{% url "api-so-line-list" %}', {
- fields: fields,
- method: 'POST',
- title: '{% trans "Add Line Item" %}',
- onSuccess: function(response) {
- handleFormSuccess(response, options);
- },
- });
-}
-
-
-/*
- * 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: {},
- target_date: {
- icon: 'fa-calendar-alt',
- },
- link: {
- icon: 'fa-link',
- },
- 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" %}',
- };
- }
-
- return fields;
-}
-
-
-/*
- * Edit an existing PurchaseOrder
- */
-function editPurchaseOrder(pk, options={}) {
-
- var fields = purchaseOrderFields(options);
-
- constructForm(`/api/order/po/${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(`/api/order/po/${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);
- }
- });
-}
-
-
-// Create a new PurchaseOrderLineItem
-function createPurchaseOrderLineItem(order, options={}) {
-
- var fields = poLineItemFields({
- order: order,
- supplier: options.supplier,
- currency: options.currency,
- target_date: options.target_date,
- });
-
- constructForm('{% url "api-po-line-list" %}', {
- fields: fields,
- method: 'POST',
- title: '{% trans "Add Line Item" %}',
- onSuccess: function(response) {
- handleFormSuccess(response, options);
- }
- });
-}
-
-
-/* Construct a set of fields for the SalesOrderLineItem form */
-function soLineItemFields(options={}) {
-
- let fields = {
- order: {
- hidden: true,
- },
- part: {
- icon: 'fa-shapes',
- },
- quantity: {},
- reference: {},
- sale_price: {
- icon: 'fa-dollar-sign',
- },
- sale_price_currency: {
- icon: 'fa-coins',
- },
- target_date: {
- icon: 'fa-calendar-alt',
- },
- notes: {
- icon: 'fa-sticky-note',
- },
- };
-
- if (options.order) {
- fields.order.value = options.order;
- }
-
- if (options.target_date) {
- fields.target_date.value = options.target_date;
- }
-
- return fields;
-}
-
-
/* Construct a set of fields for a OrderExtraLine form */
function extraLineFields(options={}) {
@@ -853,150 +86,6 @@ function extraLineFields(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_size != 1, add a note to the field
- var pack_size = 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(`/api/company/part/${value}/`,
- {
- part_detail: true,
- },
- {
- success: function(response) {
- // Extract information from the returned query
- pack_size = response.pack_size || 1;
- units = response.part_detail.units || '';
- },
- }
- ).then(function() {
- // Update pack size information
- if (pack_size != 1) {
- var txt = ` {% trans "Pack Quantity" %}: ${pack_size} ${units}`;
- $(opts.modal).find('#hint_id_quantity').after(`
${txt}
`);
- }
- }).then(function() {
- // Update pricing data (if available)
- 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) {
- var idx = 0;
- var 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',
- },
- };
-
- 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;
-}
-
function removeOrderRowFromOrderWizard(e) {
/* Remove a part selection from an order form. */
@@ -1010,66 +99,10 @@ function removeOrderRowFromOrderWizard(e) {
$('#' + 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');
- }
- }
- );
- }
- });
-}
-
/**
* Export an order (PurchaseOrder or SalesOrder)
*
* - Display a simple form which presents the user with export options
- *
*/
function exportOrder(redirect_url, options={}) {
@@ -1109,2815 +142,6 @@ 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 = [];
-
- 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 = `
-
- `;
-
- var supplier_part_input = constructField(
- `part_${pk}`,
- {
- type: 'related field',
- required: true,
- prefixRaw: supplier_part_prefix,
- },
- {
- hideLabels: true,
- }
- );
-
- var purchase_order_prefix = `
-
- `;
-
- var purchase_order_input = constructField(
- `order_${pk}`,
- {
- type: 'related field',
- required: true,
- prefixRaw: purchase_order_prefix,
- },
- {
- hideLabels: 'true',
- }
- );
-
- var buttons = `
`;
-
- 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 += `
`;
-
- var html = `
-
-
${thumb} ${part.full_name}
-
${supplier_part_input}
-
${purchase_order_input}
-
${quantity_input}
-
${buttons}
-
`;
-
- 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 += `
-
-
-
-
{% trans "Part" %}
-
{% trans "Supplier Part" %}
-
{% trans "Purchase Order" %}
-
{% trans "Quantity" %}
-
-
-
-
- ${table_entries}
-
-
- `;
-
- // 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 filtres 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_size = 1;
- var units = '';
-
- $(opts.modal).find(`#info-pack-size-${pk}`).remove();
-
- if (value != null) {
- inventreeGet(
- `/api/company/part/${value}/`,
- {
- part_detail: true,
- },
- {
- success: function(response) {
- pack_size = response.pack_size || 1;
- units = response.part_detail.units || '';
- }
- }
- ).then(function() {
- if (pack_size != 1) {
- var txt = ` {% trans "Pack Quantity" %}: ${pack_size} ${units}`;
- $(opts.modal).find(`#id_quantity_${pk}`).after(`
${txt}
`);
- }
- });
- }
- };
-
- 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(`/api/part/${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
- );
- }
- });
- });
- }
- });
-
-}
-
-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 = 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;
- }
-
- // Prepend toggles to the quantity input
- var toggle_batch = `
-
-
-
- `;
-
- var toggle_serials = `
-
-
-
- `;
-
- var units = line_item.part_detail.units || '';
- var pack_size = line_item.supplier_part_detail.pack_size || 1;
- var pack_size_div = '';
-
- var received = quantity * pack_size;
-
- if (pack_size != 1) {
- pack_size_div = `
-
- {% trans "Pack Quantity" %}: ${pack_size} ${units}
- {% trans "Received Quantity" %}: ${received} ${units}
-
`;
- }
-
- // 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" %}',
- prefixRaw: toggle_batch,
- }
- );
-
- 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" %}',
- prefixRaw: toggle_serials,
- }
- );
-
- // Hidden inputs below the "quantity" field
- var quantity_input_group = `${quantity_input}${pack_size_div}
${batch_input}
`;
-
- if (line_item.part_detail.trackable) {
- quantity_input_group += `
${sn_input}
`;
- }
-
- // 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 buttons = `
`;
- var pk = row.pk;
-
- if (row.shipment_date) {
- html += `{% trans "Shipped" %}`;
- } else {
- html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
- html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
- }
-
- html += '
';
-
- return html;
- },
- },
- ],
- });
-}
-
-/**
- * Display a "fulfilled" sub table, showing stock items fulfilled against a purchase order
- */
-function showFulfilledSubTable(index, row, element, options) {
- // Construct a table showing stock items which have been fulfilled against this line item
-
- if (!options.order) {
- return 'ERROR: Order ID not supplied';
- }
-
- var id = `fulfilled-table-${row.pk}`;
-
- var html = `
-
-
-
-
`;
-
- element.html(html);
-
- $(`#${id}`).bootstrapTable({
- url: '{% url "api-stock-list" %}',
- queryParams: {
- part: row.part,
- sales_order: options.order,
- location_detail: true,
- },
- showHeader: true,
- columns: [
- {
- field: 'pk',
- visible: false,
- },
- {
- field: 'stock',
- title: '{% trans "Stock Item" %}',
- formatter: function(value, row) {
- var text = '';
- if (row.serial && row.quantity == 1) {
- text = `{% trans "Serial Number" %}: ${row.serial}`;
- } else {
- text = `{% trans "Quantity" %}: ${row.quantity}`;
- }
-
- return renderLink(text, `/stock/item/${row.pk}/`);
- },
- },
- {
- field: 'location',
- title: '{% trans "Location" %}',
- formatter: function(value, row) {
- if (row.customer) {
- return renderLink(
- '{% trans "Shipped to customer" %}',
- `/company/${row.customer}/`
- );
- } else if (row.location && row.location_detail) {
- return renderLink(
- row.location_detail.pathstring,
- `/stock/location/${row.location}`,
- );
- } else {
- return `{% trans "Stock location not specified" %}`;
- }
- }
- }
- ],
- });
-}
-
var TotalPriceRef = ''; // reference to total price field
var TotalPriceOptions = {}; // options to reload the price
@@ -3937,888 +161,3 @@ function reloadTotal() {
}
);
};
-
-
-/**
- * Load a table displaying line items for a particular SalesOrder
- *
- * @param {String} table : HTML ID tag e.g. '#table'
- * @param {Object} options : object which contains:
- * - order {integer} : pk of the SalesOrder
- * - status: {integer} : status code for the order
- */
-function loadSalesOrderLineItemTable(table, options={}) {
-
- options.table = table;
-
- if (!options.pending && !global_settings.SALESORDER_EDIT_COMPLETED_ORDERS) {
- options.allow_edit = false;
- }
-
- options.params = options.params || {};
-
- if (!options.order) {
- console.error('function called without order ID');
- return;
- }
-
- if (!options.status) {
- console.error('function called without order status');
- return;
- }
-
- options.params.order = options.order;
- options.params.part_detail = true;
- options.params.allocations = true;
-
- var filters = loadTableFilters('salesorderlineitem');
-
- for (var key in options.params) {
- filters[key] = options.params[key];
- }
-
- options.url = options.url || '{% url "api-so-line-list" %}';
-
- var filter_target = options.filter_target || '#filter-list-sales-order-lines';
-
- setupFilterList(
- 'salesorderlineitem',
- $(table),
- filter_target,
- {
- download: true,
- }
- );
-
- // Is the order pending?
- var pending = options.pending;
-
- // Has the order shipped?
- var shipped = options.status == {{ SalesOrderStatus.SHIPPED }};
-
- // Show detail view if the PurchaseOrder is PENDING or SHIPPED
- var show_detail = pending || shipped;
-
- // Add callbacks for expand / collapse buttons
- $('#sales-lines-expand').click(function() {
- $(table).bootstrapTable('expandAllRows');
- });
-
- $('#sales-lines-collapse').click(function() {
- $(table).bootstrapTable('collapseAllRows');
- });
-
- // Table columns to display
- var columns = [
- /*
- {
- checkbox: true,
- visible: true,
- switchable: false,
- },
- */
- {
- sortable: true,
- sortName: 'part_detail.name',
- field: 'part',
- 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/${value}/`);
- } else {
- return '-';
- }
- },
- footerFormatter: function() {
- return '{% trans "Total" %}';
- },
- },
- {
- sortable: true,
- field: 'reference',
- title: '{% trans "Reference" %}',
- switchable: true,
- },
- {
- 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);
- },
- switchable: false,
- },
- {
- sortable: true,
- field: 'sale_price',
- title: '{% trans "Unit Price" %}',
- formatter: function(value, row) {
- return formatCurrency(row.sale_price, {
- currency: row.sale_price_currency
- });
- }
- },
- {
- field: 'total_price',
- sortable: true,
- title: '{% trans "Total Price" %}',
- formatter: function(value, row) {
- return formatCurrency(row.sale_price * row.quantity, {
- currency: row.sale_price_currency,
- });
- },
- footerFormatter: function(data) {
- return calculateTotalPrice(
- data,
- function(row) {
- return row.sale_price ? row.sale_price * row.quantity : null;
- },
- function(row) {
- return row.sale_price_currency;
- }
- );
- }
- },
- {
- field: 'target_date',
- title: '{% trans "Target Date" %}',
- sortable: true,
- switchable: true,
- formatter: function(value, row) {
- if (row.target_date) {
- var html = renderDate(row.target_date);
-
- if (row.overdue) {
- html += ``;
- }
-
- return html;
-
- } else if (row.order_detail && row.order_detail.target_date) {
- return `${renderDate(row.order_detail.target_date)}`;
- } else {
- return '-';
- }
- }
- }
- ];
-
- if (pending) {
- columns.push(
- {
- field: 'stock',
- title: '{% trans "Available Stock" %}',
- formatter: function(value, row) {
- var available = row.available_stock;
- var required = Math.max(row.quantity - row.allocated - row.shipped, 0);
-
- var html = '';
-
- if (available > 0) {
- var url = `/part/${row.part}/?display=part-stock`;
-
- var text = available;
-
- html = renderLink(text, url);
- } else {
- html += `{% trans "No Stock Available" %}`;
- }
-
- if (required > 0) {
- if (available >= required) {
- html += ``;
- } else {
- html += ``;
- }
- }
-
- return html;
- },
- },
- );
-
- columns.push(
- {
- field: 'allocated',
- title: '{% trans "Allocated" %}',
- switchable: false,
- sortable: true,
- formatter: function(value, row, index, field) {
- return makeProgressBar(row.allocated, row.quantity, {
- id: `order-line-progress-${row.pk}`,
- });
- },
- sorter: function(valA, valB, rowA, rowB) {
-
- var A = rowA.allocated;
- var B = rowB.allocated;
-
- if (A == 0 && B == 0) {
- return (rowA.quantity > rowB.quantity) ? 1 : -1;
- }
-
- var progressA = parseFloat(A) / rowA.quantity;
- var progressB = parseFloat(B) / rowB.quantity;
-
- return (progressA < progressB) ? 1 : -1;
- }
- },
- );
- }
-
- columns.push({
- field: 'shipped',
- title: '{% trans "Shipped" %}',
- switchable: false,
- sortable: true,
- formatter: function(value, row) {
- return makeProgressBar(row.shipped, row.quantity, {
- id: `order-line-shipped-${row.pk}`
- });
- },
- sorter: function(valA, valB, rowA, rowB) {
- var A = rowA.shipped;
- var B = rowB.shipped;
-
- if (A == 0 && B == 0) {
- return (rowA.quantity > rowB.quantity) ? 1 : -1;
- }
-
- var progressA = parseFloat(A) / rowA.quantity;
- var progressB = parseFloat(B) / rowB.quantity;
-
- return (progressA < progressB) ? 1 : -1;
- }
- });
-
- columns.push({
- field: 'notes',
- title: '{% trans "Notes" %}',
- });
-
- if (pending) {
- columns.push({
- field: 'buttons',
- switchable: false,
- formatter: function(value, row, index, field) {
-
- var html = `
`;
-
- var pk = row.pk;
-
- if (row.part) {
- var part = row.part_detail;
-
- if (part.trackable) {
- html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}');
- }
-
- html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
-
- if (part.purchaseable) {
- html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
- }
-
- if (part.assembly) {
- html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
- }
-
- html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
- }
-
- html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line item" %}');
- html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
-
- var delete_disabled = false;
-
- var title = '{% trans "Delete line item" %}';
-
- if (!!row.shipped) {
- delete_disabled = true;
- title = '{% trans "Cannot be deleted as items have been shipped" %}';
- } else if (!!row.allocated) {
- delete_disabled = true;
- title = '{% trans "Cannot be deleted as items have been allocated" %}';
- }
-
- // Prevent deletion of the line item if items have been allocated or shipped!
- html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, title, {disabled: delete_disabled});
-
- html += `
`;
-
- if (options.allow_edit) {
- var pk = row.pk;
- html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
- html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
- html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
- }
-
- html += `
`;
- return html;
- }
- });
-
- function reloadTable() {
- $(table).bootstrapTable('refresh');
- reloadTotal();
- }
-
- // Configure callback functions once the table is loaded
- function setupCallbacks() {
-
- // Callback for duplicating lines
- $(table).find('.button-duplicate').click(function() {
- var pk = $(this).attr('pk');
-
- inventreeGet(`/api/order/so-extra-line/${pk}/`, {}, {
- success: function(data) {
-
- var fields = extraLineFields();
-
- constructForm('{% url "api-so-extra-line-list" %}', {
- method: 'POST',
- fields: fields,
- data: data,
- title: '{% trans "Duplicate Line" %}',
- onSuccess: function(response) {
- $(table).bootstrapTable('refresh');
- }
- });
- }
- });
- });
-
- // Callback for editing lines
- $(table).find('.button-edit').click(function() {
- var pk = $(this).attr('pk');
-
- constructForm(`/api/order/so-extra-line/${pk}/`, {
- fields: extraLineFields(),
- title: '{% trans "Edit Line" %}',
- onSuccess: reloadTable,
- });
- });
-
- // Callback for deleting lines
- $(table).find('.button-delete').click(function() {
- var pk = $(this).attr('pk');
-
- constructForm(`/api/order/so-extra-line/${pk}/`, {
- method: 'DELETE',
- title: '{% trans "Delete Line" %}',
- onSuccess: reloadTable,
- });
- });
- }
-
- $(table).inventreeTable({
- onPostBody: setupCallbacks,
- name: 'salesorderextraline',
- sidePagination: 'client',
- formatNoMatches: function() {
- return '{% trans "No matching lines" %}';
- },
- queryParams: filters,
- original: options.params,
- url: options.url,
- showFooter: true,
- uniqueId: 'pk',
- detailViewByClick: false,
- columns: columns,
- });
-}
-
-
-/*
- * Construct a set of fields for a ReturnOrder form
- */
-function returnOrderFields(options={}) {
-
- let fields = {
- reference: {
- icon: 'fa-hashtag',
- },
- description: {},
- customer: {
- icon: 'fa-user-tie',
- secondary: {
- title: '{% trans "Add Customer" %}',
- fields: function() {
- var fields = companyFormFields();
- fields.is_customer.value = true;
- return fields;
- }
- }
- },
- customer_reference: {},
- link: {
- icon: 'fa-link',
- },
- responsible: {
- icon: 'fa-user',
- }
- };
-
- return fields;
-}
-
-
-/*
- * Create a new Return Order
- */
-function createReturnOrder(options={}) {
- let fields = returnOrderFields(options);
-
- if (options.customer) {
- fields.customer.value = options.customer;
- }
-
- constructForm('{% url "api-return-order-list" %}', {
- method: 'POST',
- fields: fields,
- title: '{% trans "Create Return Order" %}',
- onSuccess: function(data) {
- location.href = `/order/return-order/${data.pk}/`;
- },
- });
-}
-
-
-/*
- * Load a table of return orders
- */
-function loadReturnOrderTable(table, options={}) {
-
- // Ensure the table starts in a known state
- $(table).bootstrapTable('destroy');
-
- options.params = options.params || {};
- options.params['customer_detail'] = true;
-
- var filters = loadTableFilters('returnorder');
-
- for (var key in options.params) {
- filters[key] = options.params[key];
- }
-
- setupFilterList('returnorder', $(table), '#filter-list-returnorder', {download: true});
-
- let display_mode = inventreeLoad('returnorder-table-display-mode', 'list');
-
- let is_calendar = display_mode == 'calendar';
-
- $(table).inventreeTable({
- url: '{% url "api-return-order-list" %}',
- queryParams: filters,
- name: 'returnorder',
- sidePagination: 'server',
- original: options.params,
- showColumns: !is_calendar,
- search: !is_calendar,
- showCustomViewButton: false,
- showCustomView: is_calendar,
- disablePagination: is_calendar,
- formatNoMatches: function() {
- return '{% trans "No return orders found" %}';
- },
- onRefresh: function() {
- loadReturnOrderTable(table, options);
- },
- onLoadSuccess: function() {
- // TODO
- },
- columns: [
- {
- title: '',
- checkbox: true,
- visible: true,
- switchable: false,
- },
- {
- sortable: true,
- field: 'reference',
- title: '{% trans "Return Order" %}',
- formatter: function(value, row) {
- var html = renderLink(value, `/order/return-order/${row.pk}/`);
- 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 'todo';
- return salesOrderStatusDisplay(row.status);
- }
- },
- {
- sortable: true,
- field: 'creation_date',
- title: '{% trans "Creation Date" %}',
- formatter: function(value) {
- return renderDate(value);
- }
- },
- ]
- });
-}
diff --git a/InvenTree/templates/js/translated/purchase_order.js b/InvenTree/templates/js/translated/purchase_order.js
new file mode 100644
index 0000000000..f09c0084f8
--- /dev/null
+++ b/InvenTree/templates/js/translated/purchase_order.js
@@ -0,0 +1,2290 @@
+{% load i18n %}
+{% load inventree_extras %}
+
+/* globals
+ companyFormFields,
+ constructForm,
+ createSupplierPart,
+ global_settings,
+ imageHoverIcon,
+ inventreeGet,
+ launchModalForm,
+ loadTableFilters,
+ makeIconBadge,
+ purchaseOrderStatusDisplay,
+ receivePurchaseOrderItems,
+ renderLink,
+ salesOrderStatusDisplay,
+ setupFilterList,
+ supplierPartFields,
+*/
+
+/* exported
+ allocateStockToSalesOrder,
+ cancelPurchaseOrder,
+ cancelSalesOrder,
+ completePurchaseOrder,
+ completeSalesOrder,
+ completeSalesOrderShipment,
+ completePendingShipments,
+ createPurchaseOrder,
+ createPurchaseOrderLineItem,
+ createReturnOrder,
+ createSalesOrder,
+ createSalesOrderLineItem,
+ createSalesOrderShipment,
+ duplicatePurchaseOrder,
+ editPurchaseOrder,
+ editPurchaseOrderLineItem,
+ editSalesOrder,
+ exportOrder,
+ issuePurchaseOrder,
+ loadPurchaseOrderLineItemTable,
+ loadPurchaseOrderExtraLineTable
+ loadPurchaseOrderTable,
+ loadReturnOrderTable,
+ loadSalesOrderAllocationTable,
+ loadSalesOrderLineItemTable,
+ loadSalesOrderExtraLineTable
+ loadSalesOrderShipmentTable,
+ loadSalesOrderTable,
+ newPurchaseOrderFromOrderWizard,
+ newSupplierPartFromOrderWizard,
+ orderParts,
+ removeOrderRowFromOrderWizard,
+ removePurchaseOrderLineItem,
+ loadOrderTotal,
+ extraLineFields,
+*/
+
+
+
+/*
+ * 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: {},
+ target_date: {
+ icon: 'fa-calendar-alt',
+ },
+ link: {
+ icon: 'fa-link',
+ },
+ 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" %}',
+ };
+ }
+
+ return fields;
+}
+
+
+/*
+ * Edit an existing PurchaseOrder
+ */
+function editPurchaseOrder(pk, options={}) {
+
+ var fields = purchaseOrderFields(options);
+
+ constructForm(`/api/order/po/${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(`/api/order/po/${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_size != 1, add a note to the field
+ var pack_size = 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(`/api/company/part/${value}/`,
+ {
+ part_detail: true,
+ },
+ {
+ success: function(response) {
+ // Extract information from the returned query
+ pack_size = response.pack_size || 1;
+ units = response.part_detail.units || '';
+ },
+ }
+ ).then(function() {
+ // Update pack size information
+ if (pack_size != 1) {
+ var txt = ` {% trans "Pack Quantity" %}: ${pack_size} ${units}`;
+ $(opts.modal).find('#hint_id_quantity').after(`
${txt}
`);
+ }
+ }).then(function() {
+ // Update pricing data (if available)
+ 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) {
+ var idx = 0;
+ var 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',
+ },
+ };
+
+ 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,
+ });
+
+ 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 = `
+
+ {% trans "Mark this order as complete?" %}
+
`;
+
+ if (opts.context.is_complete) {
+ html += `
+
+ {% trans "All line items have been received" %}
+
`;
+ } else {
+ html += `
+
+ {% trans 'This order has line items which have not been marked as received.' %}
+ {% trans 'Completing this order means that the order and line items will no longer be editable.' %}
+
`;
+ }
+
+ 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 = `
+
+ {% trans "Are you sure you wish to cancel this purchase order?" %}
+
`;
+
+ if (!opts.context.can_cancel) {
+ html += `
+
+ {% trans "This purchase order can not be cancelled" %}
+
`;
+ }
+
+ return html;
+ },
+ onSuccess: function(response) {
+ handleFormSuccess(response, options);
+ }
+ }
+ );
+}
+
+
+/*
+ * Launches a modal form to mark a PurchaseOrder as "issued"
+ */
+function issuePurchaseOrder(order_id, options={}) {
+
+ constructForm(
+ `/api/order/po/${order_id}/issue/`,
+ {
+ method: 'POST',
+ title: '{% trans "Issue Purchase Order" %}',
+ confirm: true,
+ preFormContent: function(opts) {
+ var html = `
+
+ {% trans 'After placing this purchase order, line items will no longer be editable.' %}
+
`;
+
+ return 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 = `
+
+ `;
+
+ var supplier_part_input = constructField(
+ `part_${pk}`,
+ {
+ type: 'related field',
+ required: true,
+ prefixRaw: supplier_part_prefix,
+ },
+ {
+ hideLabels: true,
+ }
+ );
+
+ var purchase_order_prefix = `
+
+ `;
+
+ var purchase_order_input = constructField(
+ `order_${pk}`,
+ {
+ type: 'related field',
+ required: true,
+ prefixRaw: purchase_order_prefix,
+ },
+ {
+ hideLabels: 'true',
+ }
+ );
+
+ var buttons = `
`;
+
+ 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 += `
`;
+
+ var html = `
+
+
${thumb} ${part.full_name}
+
${supplier_part_input}
+
${purchase_order_input}
+
${quantity_input}
+
${buttons}
+
`;
+
+ 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 += `
+
+
+
+
{% trans "Part" %}
+
{% trans "Supplier Part" %}
+
{% trans "Purchase Order" %}
+
{% trans "Quantity" %}
+
+
+
+
+ ${table_entries}
+
+
+ `;
+
+ // 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 filtres 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_size = 1;
+ var units = '';
+
+ $(opts.modal).find(`#info-pack-size-${pk}`).remove();
+
+ if (value != null) {
+ inventreeGet(
+ `/api/company/part/${value}/`,
+ {
+ part_detail: true,
+ },
+ {
+ success: function(response) {
+ pack_size = response.pack_size || 1;
+ units = response.part_detail.units || '';
+ }
+ }
+ ).then(function() {
+ if (pack_size != 1) {
+ var txt = ` {% trans "Pack Quantity" %}: ${pack_size} ${units}`;
+ $(opts.modal).find(`#id_quantity_${pk}`).after(`
${txt}
`);
+ }
+ });
+ }
+ };
+
+ 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(`/api/part/${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;
+ }
+
+ // Prepend toggles to the quantity input
+ var toggle_batch = `
+
+
+
+ `;
+
+ var toggle_serials = `
+
+
+
+ `;
+
+ var units = line_item.part_detail.units || '';
+ var pack_size = line_item.supplier_part_detail.pack_size || 1;
+ var pack_size_div = '';
+
+ var received = quantity * pack_size;
+
+ if (pack_size != 1) {
+ pack_size_div = `
+
+ {% trans "Pack Quantity" %}: ${pack_size} ${units}
+ {% trans "Received Quantity" %}: ${received} ${units}
+
`;
+ }
+
+ // 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" %}',
+ prefixRaw: toggle_batch,
+ }
+ );
+
+ 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" %}',
+ prefixRaw: toggle_serials,
+ }
+ );
+
+ // Hidden inputs below the "quantity" field
+ var quantity_input_group = `${quantity_input}${pack_size_div}
${batch_input}
`;
+
+ if (line_item.part_detail.trackable) {
+ quantity_input_group += `
${sn_input}
`;
+ }
+
+ // 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 buttons = `
+ `;
+
+ if (!pending_shipments.length) {
+ html += `
+ {% trans "No pending shipments found" %}
+ `;
+ } else {
+ html += `
+ {% trans "No stock items have been allocated to pending shipments" %}
+ `;
+ }
+
+ html += `
+
`;
+
+ if (opts.context.pending_shipments) {
+ html += `
+
+ {% trans "Order cannot be completed as there are incomplete shipments" %}
+
`;
+ }
+
+ if (!opts.context.is_complete) {
+ html += `
+
+ {% trans "This order has line items which have not been completed." %}
+ {% trans "Completing this order means that the order and line items will no longer be editable." %}
+
`;
+ }
+
+ return html;
+ },
+ onSuccess: function(response) {
+ handleFormSuccess(response, options);
+ }
+ }
+ );
+}
+
+
+/*
+ * Launches a modal form to mark a SalesOrder as "cancelled"
+ */
+function cancelSalesOrder(order_id, options={}) {
+
+ constructForm(
+ `/api/order/so/${order_id}/cancel/`,
+ {
+ method: 'POST',
+ title: '{% trans "Cancel Sales Order" %}',
+ confirm: true,
+ preFormContent: function(opts) {
+ var html = `
+
+ {% trans "Cancelling this order means that the order will no longer be editable." %}
+
`;
+
+ return html;
+ },
+ onSuccess: function(response) {
+ handleFormSuccess(response, options);
+ }
+ }
+ );
+}
+
+// Open a dialog to create a new sales order shipment
+function createSalesOrderShipment(options={}) {
+
+ // Work out the next shipment number for the given order
+ inventreeGet(
+ '{% url "api-so-shipment-list" %}',
+ {
+ order: options.order,
+ },
+ {
+ success: function(results) {
+ // "predict" the next reference number
+ var ref = results.length + 1;
+
+ var found = false;
+
+ while (!found) {
+
+ var no_match = true;
+
+ for (var ii = 0; ii < results.length; ii++) {
+ if (ref.toString() == results[ii].reference.toString()) {
+ no_match = false;
+ break;
+ }
+ }
+
+ if (no_match) {
+ break;
+ } else {
+ ref++;
+ }
+ }
+
+ var fields = salesOrderShipmentFields(options);
+
+ fields.reference.value = ref;
+ fields.reference.prefix = options.reference;
+
+ constructForm('{% url "api-so-shipment-list" %}', {
+ method: 'POST',
+ fields: fields,
+ title: '{% trans "Create New Shipment" %}',
+ onSuccess: function(data) {
+ if (options.onSuccess) {
+ options.onSuccess(data);
+ }
+ }
+ });
+ }
+ }
+ );
+}
+
+
+
+/*
+ * Load table displaying list of sales orders
+ */
+function loadSalesOrderTable(table, options) {
+
+ // Ensure the table starts in a known state
+ $(table).bootstrapTable('destroy');
+
+ 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" %}';
+
+ var target = '#filter-list-salesorder';
+
+ setupFilterList('salesorder', $(table), target, {download: true});
+
+ var display_mode = inventreeLoad('salesorder-table-display-mode', 'list');
+
+ 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.customer_detail = true;
+ filters.min_date = start;
+ filters.max_date = end;
+
+ // Request orders from the server within specified date range
+ inventreeGet(
+ '{% url "api-so-list" %}',
+ filters,
+ {
+ success: function(response) {
+
+ for (var idx = 0; idx < response.length; idx++) {
+ var order = response[idx];
+
+ var date = order.creation_date;
+
+ if (order.shipment_date) {
+ date = order.shipment_date;
+ } else if (order.target_date) {
+ date = order.target_date;
+ }
+
+ var title = `${order.reference} - ${order.customer_detail.name}`;
+
+ // Default color is blue
+ var color = '#4c68f5';
+
+ // Overdue orders are red
+ if (order.overdue) {
+ color = '#c22525';
+ } else if (order.status == {{ SalesOrderStatus.SHIPPED }}) {
+ color = '#25c235';
+ }
+
+ var event = {
+ title: title,
+ start: date,
+ end: date,
+ url: `/order/sales-order/${order.pk}/`,
+ backgroundColor: color,
+ };
+
+ calendar.addEvent(event);
+ }
+ }
+ }
+ );
+ }
+
+ $(table).inventreeTable({
+ url: options.url,
+ queryParams: filters,
+ name: 'salesorder',
+ groupBy: false,
+ sidePagination: 'server',
+ original: options.params,
+ showColums: display_mode != 'calendar',
+ search: display_mode != 'calendar',
+ showCustomViewButton: false,
+ showCustomView: display_mode == 'calendar',
+ disablePagination: display_mode == 'calendar',
+ formatNoMatches: function() {
+ return '{% trans "No sales orders found" %}';
+ },
+ buttons: constructOrderTableButtons({
+ prefix: 'salesorder',
+ disableTreeView: true,
+ callback: function() {
+ // Reload the entire table
+ loadSalesOrderTable(table, options);
+ },
+ }),
+ customView: function(data) {
+ return ``;
+ },
+ onRefresh: function() {
+ loadSalesOrderTable(table, options);
+ },
+ onLoadSuccess: function() {
+
+ if (display_mode == 'calendar') {
+ var el = document.getElementById('purchase-order-calendar');
+
+ calendar = new FullCalendar.Calendar(el, {
+ initialView: 'dayGridMonth',
+ nowIndicator: true,
+ aspectRatio: 2.5,
+ locale: options.locale,
+ datesSet: function() {
+ buildEvents(calendar);
+ }
+ });
+
+ calendar.render();
+ }
+ },
+ columns: [
+ {
+ title: '',
+ checkbox: true,
+ visible: true,
+ switchable: false,
+ },
+ {
+ sortable: true,
+ field: 'reference',
+ title: '{% trans "Sales Order" %}',
+ formatter: function(value, row) {
+ 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);
+ }
+ },
+ {
+ sortable: true,
+ field: 'creation_date',
+ title: '{% trans "Creation Date" %}',
+ formatter: function(value) {
+ return renderDate(value);
+ }
+ },
+ {
+ sortable: true,
+ field: 'target_date',
+ title: '{% trans "Target Date" %}',
+ formatter: function(value) {
+ return renderDate(value);
+ }
+ },
+ {
+ sortable: true,
+ field: 'shipment_date',
+ title: '{% trans "Shipment Date" %}',
+ formatter: function(value) {
+ return renderDate(value);
+ }
+ },
+ {
+ sortable: true,
+ field: 'line_items',
+ title: '{% trans "Items" %}'
+ },
+ {
+ field: 'total_price',
+ title: '{% trans "Total Cost" %}',
+ switchable: true,
+ sortable: true,
+ formatter: function(value, row) {
+ return formatCurrency(value, {
+ currency: row.total_price_currency,
+ });
+ }
+ }
+ ],
+ });
+}
+
+
+/*
+ * Load a table displaying Shipment information against a particular order
+ */
+function loadSalesOrderShipmentTable(table, options={}) {
+
+ options.table = table;
+
+ options.params = options.params || {};
+
+ // Filter by order
+ options.params.order = options.order;
+
+ // Filter by "shipped" status
+ options.params.shipped = options.shipped || false;
+
+ var filters = loadTableFilters('salesordershipment');
+
+ for (var key in options.params) {
+ filters[key] = options.params[key];
+ }
+
+ setupFilterList('salesordershipment', $(table), options.filter_target);
+
+ // Add callbacks for expand / collapse buttons
+ var prefix = options.shipped ? 'completed' : 'pending';
+
+ $(`#${prefix}-shipments-expand`).click(function() {
+ $(table).bootstrapTable('expandAllRows');
+ });
+
+ $(`#${prefix}-shipments-collapse`).click(function() {
+ $(table).bootstrapTable('collapseAllRows');
+ });
+
+ function makeShipmentActions(row) {
+ // Construct "actions" for the given shipment row
+ var pk = row.pk;
+
+ var html = `
`;
+
+ html += makeIconButton('fa-edit icon-blue', 'button-shipment-edit', pk, '{% trans "Edit shipment" %}');
+
+ if (!options.shipped) {
+ html += makeIconButton('fa-truck icon-green', 'button-shipment-ship', pk, '{% trans "Complete shipment" %}');
+ }
+
+ var enable_delete = row.allocations && row.allocations.length == 0;
+
+ html += makeIconButton('fa-trash-alt icon-red', 'button-shipment-delete', pk, '{% trans "Delete shipment" %}', {disabled: !enable_delete});
+
+ html += `
`;
+ var pk = row.pk;
+
+ if (row.shipment_date) {
+ html += `{% trans "Shipped" %}`;
+ } else {
+ html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
+ html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
+ }
+
+ html += '
';
+
+ return html;
+ },
+ },
+ ],
+ });
+}
+
+/**
+ * Display a "fulfilled" sub table, showing stock items fulfilled against a purchase order
+ */
+function showFulfilledSubTable(index, row, element, options) {
+ // Construct a table showing stock items which have been fulfilled against this line item
+
+ if (!options.order) {
+ return 'ERROR: Order ID not supplied';
+ }
+
+ var id = `fulfilled-table-${row.pk}`;
+
+ var html = `
+
+
+
+
`;
+
+ element.html(html);
+
+ $(`#${id}`).bootstrapTable({
+ url: '{% url "api-stock-list" %}',
+ queryParams: {
+ part: row.part,
+ sales_order: options.order,
+ location_detail: true,
+ },
+ showHeader: true,
+ columns: [
+ {
+ field: 'pk',
+ visible: false,
+ },
+ {
+ field: 'stock',
+ title: '{% trans "Stock Item" %}',
+ formatter: function(value, row) {
+ var text = '';
+ if (row.serial && row.quantity == 1) {
+ text = `{% trans "Serial Number" %}: ${row.serial}`;
+ } else {
+ text = `{% trans "Quantity" %}: ${row.quantity}`;
+ }
+
+ return renderLink(text, `/stock/item/${row.pk}/`);
+ },
+ },
+ {
+ field: 'location',
+ title: '{% trans "Location" %}',
+ formatter: function(value, row) {
+ if (row.customer) {
+ return renderLink(
+ '{% trans "Shipped to customer" %}',
+ `/company/${row.customer}/`
+ );
+ } else if (row.location && row.location_detail) {
+ return renderLink(
+ row.location_detail.pathstring,
+ `/stock/location/${row.location}`,
+ );
+ } else {
+ return `{% trans "Stock location not specified" %}`;
+ }
+ }
+ }
+ ],
+ });
+}
+
+
+
+/**
+ * Load a table displaying line items for a particular SalesOrder
+ *
+ * @param {String} table : HTML ID tag e.g. '#table'
+ * @param {Object} options : object which contains:
+ * - order {integer} : pk of the SalesOrder
+ * - status: {integer} : status code for the order
+ */
+function loadSalesOrderLineItemTable(table, options={}) {
+
+ options.table = table;
+
+ if (!options.pending && !global_settings.SALESORDER_EDIT_COMPLETED_ORDERS) {
+ options.allow_edit = false;
+ }
+
+ options.params = options.params || {};
+
+ if (!options.order) {
+ console.error('function called without order ID');
+ return;
+ }
+
+ if (!options.status) {
+ console.error('function called without order status');
+ return;
+ }
+
+ options.params.order = options.order;
+ options.params.part_detail = true;
+ options.params.allocations = true;
+
+ var filters = loadTableFilters('salesorderlineitem');
+
+ for (var key in options.params) {
+ filters[key] = options.params[key];
+ }
+
+ options.url = options.url || '{% url "api-so-line-list" %}';
+
+ var filter_target = options.filter_target || '#filter-list-sales-order-lines';
+
+ setupFilterList(
+ 'salesorderlineitem',
+ $(table),
+ filter_target,
+ {
+ download: true,
+ }
+ );
+
+ // Is the order pending?
+ var pending = options.pending;
+
+ // Has the order shipped?
+ var shipped = options.status == {{ SalesOrderStatus.SHIPPED }};
+
+ // Show detail view if the PurchaseOrder is PENDING or SHIPPED
+ var show_detail = pending || shipped;
+
+ // Add callbacks for expand / collapse buttons
+ $('#sales-lines-expand').click(function() {
+ $(table).bootstrapTable('expandAllRows');
+ });
+
+ $('#sales-lines-collapse').click(function() {
+ $(table).bootstrapTable('collapseAllRows');
+ });
+
+ // Table columns to display
+ var columns = [
+ /*
+ {
+ checkbox: true,
+ visible: true,
+ switchable: false,
+ },
+ */
+ {
+ sortable: true,
+ sortName: 'part_detail.name',
+ field: 'part',
+ 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/${value}/`);
+ } else {
+ return '-';
+ }
+ },
+ footerFormatter: function() {
+ return '{% trans "Total" %}';
+ },
+ },
+ {
+ sortable: true,
+ field: 'reference',
+ title: '{% trans "Reference" %}',
+ switchable: true,
+ },
+ {
+ 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);
+ },
+ switchable: false,
+ },
+ {
+ sortable: true,
+ field: 'sale_price',
+ title: '{% trans "Unit Price" %}',
+ formatter: function(value, row) {
+ return formatCurrency(row.sale_price, {
+ currency: row.sale_price_currency
+ });
+ }
+ },
+ {
+ field: 'total_price',
+ sortable: true,
+ title: '{% trans "Total Price" %}',
+ formatter: function(value, row) {
+ return formatCurrency(row.sale_price * row.quantity, {
+ currency: row.sale_price_currency,
+ });
+ },
+ footerFormatter: function(data) {
+ return calculateTotalPrice(
+ data,
+ function(row) {
+ return row.sale_price ? row.sale_price * row.quantity : null;
+ },
+ function(row) {
+ return row.sale_price_currency;
+ }
+ );
+ }
+ },
+ {
+ field: 'target_date',
+ title: '{% trans "Target Date" %}',
+ sortable: true,
+ switchable: true,
+ formatter: function(value, row) {
+ if (row.target_date) {
+ var html = renderDate(row.target_date);
+
+ if (row.overdue) {
+ html += ``;
+ }
+
+ return html;
+
+ } else if (row.order_detail && row.order_detail.target_date) {
+ return `${renderDate(row.order_detail.target_date)}`;
+ } else {
+ return '-';
+ }
+ }
+ }
+ ];
+
+ if (pending) {
+ columns.push(
+ {
+ field: 'stock',
+ title: '{% trans "Available Stock" %}',
+ formatter: function(value, row) {
+ var available = row.available_stock;
+ var required = Math.max(row.quantity - row.allocated - row.shipped, 0);
+
+ var html = '';
+
+ if (available > 0) {
+ var url = `/part/${row.part}/?display=part-stock`;
+
+ var text = available;
+
+ html = renderLink(text, url);
+ } else {
+ html += `{% trans "No Stock Available" %}`;
+ }
+
+ if (required > 0) {
+ if (available >= required) {
+ html += ``;
+ } else {
+ html += ``;
+ }
+ }
+
+ return html;
+ },
+ },
+ );
+
+ columns.push(
+ {
+ field: 'allocated',
+ title: '{% trans "Allocated" %}',
+ switchable: false,
+ sortable: true,
+ formatter: function(value, row, index, field) {
+ return makeProgressBar(row.allocated, row.quantity, {
+ id: `order-line-progress-${row.pk}`,
+ });
+ },
+ sorter: function(valA, valB, rowA, rowB) {
+
+ var A = rowA.allocated;
+ var B = rowB.allocated;
+
+ if (A == 0 && B == 0) {
+ return (rowA.quantity > rowB.quantity) ? 1 : -1;
+ }
+
+ var progressA = parseFloat(A) / rowA.quantity;
+ var progressB = parseFloat(B) / rowB.quantity;
+
+ return (progressA < progressB) ? 1 : -1;
+ }
+ },
+ );
+ }
+
+ columns.push({
+ field: 'shipped',
+ title: '{% trans "Shipped" %}',
+ switchable: false,
+ sortable: true,
+ formatter: function(value, row) {
+ return makeProgressBar(row.shipped, row.quantity, {
+ id: `order-line-shipped-${row.pk}`
+ });
+ },
+ sorter: function(valA, valB, rowA, rowB) {
+ var A = rowA.shipped;
+ var B = rowB.shipped;
+
+ if (A == 0 && B == 0) {
+ return (rowA.quantity > rowB.quantity) ? 1 : -1;
+ }
+
+ var progressA = parseFloat(A) / rowA.quantity;
+ var progressB = parseFloat(B) / rowB.quantity;
+
+ return (progressA < progressB) ? 1 : -1;
+ }
+ });
+
+ columns.push({
+ field: 'notes',
+ title: '{% trans "Notes" %}',
+ });
+
+ if (pending) {
+ columns.push({
+ field: 'buttons',
+ switchable: false,
+ formatter: function(value, row, index, field) {
+
+ var html = `
`;
+
+ var pk = row.pk;
+
+ if (row.part) {
+ var part = row.part_detail;
+
+ if (part.trackable) {
+ html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}');
+ }
+
+ html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
+
+ if (part.purchaseable) {
+ html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
+ }
+
+ if (part.assembly) {
+ html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
+ }
+
+ html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
+ }
+
+ html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line item" %}');
+ html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
+
+ var delete_disabled = false;
+
+ var title = '{% trans "Delete line item" %}';
+
+ if (!!row.shipped) {
+ delete_disabled = true;
+ title = '{% trans "Cannot be deleted as items have been shipped" %}';
+ } else if (!!row.allocated) {
+ delete_disabled = true;
+ title = '{% trans "Cannot be deleted as items have been allocated" %}';
+ }
+
+ // Prevent deletion of the line item if items have been allocated or shipped!
+ html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, title, {disabled: delete_disabled});
+
+ html += `