2
0
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:
Oliver
2022-09-08 09:49:14 +10:00
committed by GitHub
parent 890c998420
commit 198ac9b275
17 changed files with 567 additions and 60 deletions

View File

@ -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) {

View File

@ -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',

View File

@ -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',