mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Feature: Supplier part pack size (#3644)
* Adds 'pack_size' field to SupplierPart model * Edit pack_size for SupplierPart via API * Display pack size in supplier part page template * Improve table ordering for SupplierPart table * Fix for API filtering - Need to use custom filter class * Adds functionality to duplicate an existing SupplierPart * Bump API version number * Display annotation of pack size in purchase order line item table * Display additional information in part purchase order table * Add UOM to purchase order table * Improve receive items functionality * Indicate quantity which will be received in modal form * Update the received quantity as the user changes the value * Take the pack_size into account when receiving line items * Take supplierpart pack size into account when receiving line items * Add "pack size" column to purchase order line item table * Tweak supplier part table * Update 'on_order' queryset annotation to take pack_size into account - May god have mercy on my soul * Adds a unit test to validate that the on_order queryset annotation is working as expected * Update Part.on_order method to take pack_size into account - Check in existing unit test also * Fix existing unit tests - Previous unit test was actually in error - Logic for calculating "on_order" was broked * More unit tests for receiving items against a purchase order * Allow pack_size < 1 * Display pack size when adding / editing PurchaseOrderLineItem * Fix bug in part purchase order table * Update part purchase order table again * Exclude notificationmessage when exporting dataset * Also display pack size when ordering parts from secondary form * javascript linting * Change user facing strings to "Pack Quantity"
This commit is contained in:
@ -16,6 +16,7 @@
|
||||
deleteManufacturerParts,
|
||||
deleteManufacturerPartParameters,
|
||||
deleteSupplierParts,
|
||||
duplicateSupplierPart,
|
||||
editCompany,
|
||||
loadCompanyTable,
|
||||
loadManufacturerPartTable,
|
||||
@ -130,7 +131,8 @@ function supplierPartFields(options={}) {
|
||||
},
|
||||
packaging: {
|
||||
icon: 'fa-box',
|
||||
}
|
||||
},
|
||||
pack_size: {},
|
||||
};
|
||||
|
||||
if (options.part) {
|
||||
@ -198,6 +200,39 @@ function createSupplierPart(options={}) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Launch a modal form to duplicate an existing SupplierPart instance
|
||||
*/
|
||||
function duplicateSupplierPart(part, options={}) {
|
||||
|
||||
var fields = options.fields || supplierPartFields();
|
||||
|
||||
// Retrieve information for the supplied part
|
||||
inventreeGet(`/api/company/part/${part}/`, {}, {
|
||||
success: function(data) {
|
||||
|
||||
// Remove fields which we do not want to duplicate
|
||||
delete data['pk'];
|
||||
delete data['available'];
|
||||
delete data['availability_updated'];
|
||||
|
||||
constructForm(`/api/company/part/`, {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
title: '{% trans "Duplicate Supplier Part" %}',
|
||||
data: data,
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Launch a modal form to edit an existing SupplierPart instance
|
||||
*/
|
||||
function editSupplierPart(part, options={}) {
|
||||
|
||||
var fields = options.fields || supplierPartFields();
|
||||
@ -865,6 +900,7 @@ function loadSupplierPartTable(table, url, options) {
|
||||
switchable: params['part_detail'],
|
||||
sortable: true,
|
||||
field: 'part_detail.full_name',
|
||||
sortName: 'part',
|
||||
title: '{% trans "Part" %}',
|
||||
formatter: function(value, row) {
|
||||
|
||||
@ -915,6 +951,7 @@ function loadSupplierPartTable(table, url, options) {
|
||||
visible: params['manufacturer_detail'],
|
||||
switchable: params['manufacturer_detail'],
|
||||
sortable: true,
|
||||
sortName: 'manufacturer',
|
||||
field: 'manufacturer_detail.name',
|
||||
title: '{% trans "Manufacturer" %}',
|
||||
formatter: function(value, row) {
|
||||
@ -933,6 +970,7 @@ function loadSupplierPartTable(table, url, options) {
|
||||
visible: params['manufacturer_detail'],
|
||||
switchable: params['manufacturer_detail'],
|
||||
sortable: true,
|
||||
sortName: 'MPN',
|
||||
field: 'manufacturer_part_detail.MPN',
|
||||
title: '{% trans "MPN" %}',
|
||||
formatter: function(value, row) {
|
||||
@ -943,8 +981,24 @@ function loadSupplierPartTable(table, url, options) {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'packaging',
|
||||
title: '{% trans "Packaging" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'pack_size',
|
||||
title: '{% trans "Pack Quantity" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'link',
|
||||
sortable: false,
|
||||
title: '{% trans "Link" %}',
|
||||
formatter: function(value) {
|
||||
if (value) {
|
||||
@ -954,21 +1008,11 @@ function loadSupplierPartTable(table, url, options) {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'note',
|
||||
title: '{% trans "Notes" %}',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'packaging',
|
||||
title: '{% trans "Packaging" %}',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'in_stock',
|
||||
title: '{% trans "In Stock" %}',
|
||||
@ -976,7 +1020,7 @@ function loadSupplierPartTable(table, url, options) {
|
||||
},
|
||||
{
|
||||
field: 'available',
|
||||
title: '{% trans "Available" %}',
|
||||
title: '{% trans "Availability" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
if (row.availability_updated) {
|
||||
|
@ -794,6 +794,35 @@ function poLineItemFields(options={}) {
|
||||
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 = '';
|
||||
|
||||
// Remove any existing note fields
|
||||
$(opts.modal).find('#info-pack-size').remove();
|
||||
|
||||
if (value != null) {
|
||||
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() {
|
||||
|
||||
if (pack_size != 1) {
|
||||
var txt = `<span class='fas fa-info-circle icon-blue'></span> {% trans "Pack Quantity" %}: ${pack_size} ${units}`;
|
||||
$(opts.modal).find('#hint_id_quantity').after(`<div class='form-info-message' id='info-pack-size'>${txt}</div>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
method: 'POST',
|
||||
title: '{% trans "Add Supplier Part" %}',
|
||||
@ -1151,16 +1180,46 @@ function orderParts(parts_list, options={}) {
|
||||
afterRender: function(fields, opts) {
|
||||
parts.forEach(function(part) {
|
||||
|
||||
var pk = part.pk;
|
||||
|
||||
// Filter by base part
|
||||
supplier_part_filters.part = part.pk;
|
||||
supplier_part_filters.part = pk;
|
||||
|
||||
if (part.manufacturer_part) {
|
||||
// Filter by manufacturer part
|
||||
supplier_part_filters.manufacturer_part = part.manufacturer_part;
|
||||
}
|
||||
|
||||
// Configure the "supplier part" field
|
||||
initializeRelatedField({
|
||||
// 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 = `<span class='fas fa-info-circle icon-blue'></span> {% trans "Pack Quantity" %}: ${pack_size} ${units}`;
|
||||
$(opts.modal).find(`#id_quantity_${pk}`).after(`<div class='form-info-message' id='info-pack-size-${pk}'>${txt}</div>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var supplier_part_field = {
|
||||
name: `part_${part.pk}`,
|
||||
model: 'supplierpart',
|
||||
api_url: '{% url "api-supplier-part-list" %}',
|
||||
@ -1169,10 +1228,15 @@ function orderParts(parts_list, options={}) {
|
||||
auto_fill: true,
|
||||
value: options.supplier_part,
|
||||
filters: supplier_part_filters,
|
||||
onEdit: onSupplierPartChanged,
|
||||
noResults: function(query) {
|
||||
return '{% trans "No matching supplier parts" %}';
|
||||
}
|
||||
}, null, opts);
|
||||
};
|
||||
|
||||
// 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({
|
||||
@ -1394,6 +1458,20 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
</span>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "Pack Quantity" %}: ${pack_size} ${units}<br>
|
||||
{% trans "Received Quantity" %}: <span class='pack_received_quantity' id='items_received_quantity_${pk}'>${received}</span> ${units}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Quantity to Receive
|
||||
var quantity_input = constructField(
|
||||
`items_quantity_${pk}`,
|
||||
@ -1433,7 +1511,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
);
|
||||
|
||||
// Hidden inputs below the "quantity" field
|
||||
var quantity_input_group = `${quantity_input}<div class='collapse' id='div-batch-${pk}'>${batch_input}</div>`;
|
||||
var quantity_input_group = `${quantity_input}${pack_size_div}<div class='collapse' id='div-batch-${pk}'>${batch_input}</div>`;
|
||||
|
||||
if (line_item.part_detail.trackable) {
|
||||
quantity_input_group += `<div class='collapse' id='div-serials-${pk}'>${sn_input}</div>`;
|
||||
@ -1545,7 +1623,9 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
var table_entries = '';
|
||||
|
||||
line_items.forEach(function(item) {
|
||||
table_entries += renderLineItem(item);
|
||||
if (item.received < item.quantity) {
|
||||
table_entries += renderLineItem(item);
|
||||
}
|
||||
});
|
||||
|
||||
var html = ``;
|
||||
@ -1581,7 +1661,8 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
confirmMessage: '{% trans "Confirm receipt of items" %}',
|
||||
title: '{% trans "Receive Purchase Order Items" %}',
|
||||
afterRender: function(fields, opts) {
|
||||
// Initialize the "destination" field for each item
|
||||
|
||||
// Run initialization routines for each line in the form
|
||||
line_items.forEach(function(item) {
|
||||
|
||||
var pk = item.pk;
|
||||
@ -1602,18 +1683,21 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
render_description: false,
|
||||
};
|
||||
|
||||
// Initialize the location field
|
||||
initializeRelatedField(
|
||||
field_details,
|
||||
null,
|
||||
opts,
|
||||
);
|
||||
|
||||
// Add 'clear' button callback for the location field
|
||||
addClearCallback(
|
||||
name,
|
||||
field_details,
|
||||
opts
|
||||
);
|
||||
|
||||
// Setup stock item status field
|
||||
initializeChoiceField(
|
||||
{
|
||||
name: `items_status_${pk}`,
|
||||
@ -1621,6 +1705,19 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
null,
|
||||
opts
|
||||
);
|
||||
|
||||
// Add change callback for quantity field
|
||||
if (item.supplier_part_detail.pack_size != 1) {
|
||||
$(opts.modal).find(`#id_items_quantity_${pk}`).change(function() {
|
||||
var value = $(opts.modal).find(`#id_items_quantity_${pk}`).val();
|
||||
|
||||
var el = $(opts.modal).find(`#quantity_${pk}`).find('.pack_received_quantity');
|
||||
|
||||
var actual = value * item.supplier_part_detail.pack_size;
|
||||
actual = formatDecimal(actual);
|
||||
el.text(actual);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add callbacks to remove rows
|
||||
@ -2158,6 +2255,23 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
switchable: false,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
formatter: function(value, row) {
|
||||
var units = '';
|
||||
|
||||
if (row.part_detail.units) {
|
||||
units = ` ${row.part_detail.units}`;
|
||||
}
|
||||
|
||||
var data = value;
|
||||
|
||||
if (row.supplier_part_detail.pack_size != 1.0) {
|
||||
var pack_size = row.supplier_part_detail.pack_size;
|
||||
var total = value * pack_size;
|
||||
data += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Pack Quantity" %}: ${pack_size}${units} - {% trans "Total Quantity" %}: ${total}${units}'></span>`;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
return data.map(function(row) {
|
||||
return +row['quantity'];
|
||||
@ -2166,6 +2280,21 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
field: 'supplier_part_detail.pack_size',
|
||||
title: '{% trans "Pack Quantity" %}',
|
||||
formatter: function(value, row) {
|
||||
var units = row.part_detail.units;
|
||||
|
||||
if (units) {
|
||||
value += ` ${units}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'purchase_price',
|
||||
|
@ -1036,6 +1036,17 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
formatter: function(value, row) {
|
||||
var data = value;
|
||||
|
||||
if (row.supplier_part_detail.pack_size != 1.0) {
|
||||
var pack_size = row.supplier_part_detail.pack_size;
|
||||
var total = value * pack_size;
|
||||
data += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Pack Quantity" %}: ${pack_size} - {% trans "Total Quantity" %}: ${total}'></span>`;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'target_date',
|
||||
@ -1077,6 +1088,17 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
|
||||
field: 'received',
|
||||
title: '{% trans "Received" %}',
|
||||
switchable: true,
|
||||
formatter: function(value, row) {
|
||||
var data = value;
|
||||
|
||||
if (value > 0 && row.supplier_part_detail.pack_size != 1.0) {
|
||||
var pack_size = row.supplier_part_detail.pack_size;
|
||||
var total = value * pack_size;
|
||||
data += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Pack Quantity" %}: ${pack_size} - {% trans "Total Quantity" %}: ${total}'></span>`;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'purchase_price',
|
||||
|
Reference in New Issue
Block a user